
Go DDD实战:领域驱动设计摘要: 本篇讲解Go语言DDD领域驱动设计实战划分限界上下文确定领域边界设计聚合根保证业务一致性区分值对象与实体用领域事件解耦上下文仓储模式持久化聚合分享贫血模型导致业务逻辑散落在service的踩坑经验对比充血模型、贫血模型、DDD分层三种方案。开篇故事去年做一个库存系统Product结构体只有字段没有方法。扣库存的逻辑写在ProductService里加库存的逻辑写在InventoryService里库存预警的判断写在AlertService里。同一个商品的扣减规则散落在5个service文件改一个规则要翻10个文件。有一次产品要求库存为0时不允许下单我改了3处才改完漏了一处导致超卖。问题在于业务规则没有归属谁都能改最后没人负责正确性。后来引入DDD把库存规则收进聚合根的方法里外部只能调聚合根的接口改状态。改一个规则只动聚合根一个文件。这篇把限界上下文、聚合根、值对象、领域事件都写清楚。一、限界上下文与聚合根限界上下文是DDD的战略设计确定一个领域的边界。订单上下文里的商品和库存上下文里的商品是两个不同的概念各自有自己的模型。边界清楚了团队协作才不会打架。聚合根是限界上下文内的核心。一个聚合是一组相关对象聚合根是入口外部只能通过聚合根访问聚合内部。聚合根负责维护业务不变量。packagedomainimport(errorstime)// Product 商品聚合根// 外部只能通过方法访问不能直接改字段// 所有业务规则在聚合根方法里保证typeProductstruct{id ProductID// 商品ID值对象namestring// 商品名price Money// 价格值对象stockint// 库存status Status// 商品状态versionint// 乐观锁版本createdAt time.Time// 创建时间}// ProductID 商品ID值对象typeProductIDstruct{valuestring}// NewProductID 创建商品ID带格式校验funcNewProductID(vstring)(ProductID,error){ifv{returnProductID{},errors.New(商品ID不能为空)}returnProductID{value:v},nil}// Value 获取ID值func(p ProductID)Value()string{returnp.value}// Money 金额值对象// 值对象不可变相等比较按值typeMoneystruct{amountfloat64currencystring// 货币类型}// NewMoney 创建金额带校验funcNewMoney(amountfloat64,currencystring)(Money,error){ifamount0{returnMoney{},errors.New(金额不能为负)}ifcurrency{returnMoney{},errors.New(货币类型不能为空)}returnMoney{amount:amount,currency:currency},nil}// Status 商品状态typeStatusstringconst(StatusOnSale Statuson_sale// 在售StatusOffSale Statusoff_sale// 下架)// NewProduct 创建商品聚合根// 工厂方法保证聚合创建时就是合法的funcNewProduct(id ProductID,namestring,price Money)(*Product,error){ifname{returnnil,errors.New(商品名不能为空)}returnProduct{id:id,name:name,price:price,stock:0,status:StatusOffSale,version:0,createdAt:time.Now(),},nil}// DeductStock 扣减库存// 业务规则: 库存不足或商品下架不允许扣减func(p*Product)DeductStock(qtyint)error{// 规则1: 商品必须在售ifp.status!StatusOnSale{returnerrors.New(商品未上架不能扣减库存)}// 规则2: 库存必须充足ifp.stockqty{returnerrors.New(库存不足)}// 规则3: 扣减数量必须大于0ifqty0{returnerrors.New(扣减数量必须大于0)}p.stock-qty p.versionreturnnil}// Restock 补库存func(p*Product)Restock(qtyint)error{ifqty0{returnerrors.New(补充数量必须大于0)}p.stockqty p.versionreturnnil}// PutOnSale 上架func(p*Product)PutOnSale()error{ifp.stock0{returnerrors.New(库存为0不能上架)}p.statusStatusOnSale p.versionreturnnil}// 读取方法外部只能读不能改func(p*Product)ID()ProductID{returnp.id}func(p*Product)Name()string{returnp.name}func(p*Product)Price()Money{returnp.price}func(p*Product)Stock()int{returnp.stock}func(p*Product)Status()Status{returnp.status}func(p*Product)Version()int{returnp.version}聚合根字段不导出外部只能通过方法访问。扣库存的规则集中在DeductStock里改规则只改这一个方法。这就是充血模型业务逻辑和数据在一起。二、领域事件与仓储模式聚合根操作完会产生领域事件通知其他上下文。比如商品库存低于阈值发布库存预警事件采购上下文订阅这个事件自动生成采购单。packagedomainimport(contextfmttime)// DomainEvent 领域事件接口typeDomainEventinterface{OccurredOn()time.Time// 发生时间AggregateID()string// 聚合IDEventType()string// 事件类型}// StockLowEvent 库存预警事件typeStockLowEventstruct{productIDstringthresholdint// 预警阈值currentint// 当前库存occurredOn time.Time}// NewStockLowEvent 创建库存预警事件funcNewStockLowEvent(pidstring,threshold,currentint)*StockLowEvent{returnStockLowEvent{productID:pid,threshold:threshold,current:current,occurredOn:time.Now(),}}func(e*StockLowEvent)OccurredOn()time.Time{returne.occurredOn}func(e*StockLowEvent)AggregateID()string{returne.productID}func(e*StockLowEvent)EventType()string{returnStockLow}// DomainEventPublisher 领域事件发布器typeDomainEventPublisherinterface{Publish(ctx context.Context,event DomainEvent)error}// ProductRepository 商品仓储接口// 定义在领域层由基础设施层实现typeProductRepositoryinterface{// FindByID 按ID加载商品聚合FindByID(ctx context.Context,id ProductID)(*Product,error)// Save 保存商品聚合(新增或更新)Save(ctx context.Context,product*Product)error}// ProductService 商品应用服务// 编排领域对象不含业务规则typeProductServicestruct{repo ProductRepository publisher DomainEventPublisher}// NewProductService 创建应用服务funcNewProductService(repo ProductRepository,pub DomainEventPublisher)*ProductService{returnProductService{repo:repo,publisher:pub}}// Purchase 采购入库流程// 加载聚合 - 调聚合方法 - 保存 - 发事件func(s*ProductService)Purchase(ctx context.Context,productIDstring,qtyint,)error{// 加载聚合根pid,err:NewProductID(productID)iferr!nil{returnerr}product,err:s.repo.FindByID(ctx,pid)iferr!nil{returnfmt.Errorf(加载商品失败: %w,err)}// 记录补库存前的库存beforeStock:product.Stock()// 调聚合根方法补库存iferr:product.Restock(qty);err!nil{returnerr}// 保存聚合iferr:s.repo.Save(ctx,product);err!nil{returnfmt.Errorf(保存商品失败: %w,err)}// 库存从低补到高不发预警// 库存从高降到低才发预警这里演示补货后判断_beforeStockreturnnil}// Sell 出售扣库存// 扣库存后检查是否低于阈值低则发事件func(s*ProductService)Sell(ctx context.Context,productIDstring,qtyint,)error{pid,err:NewProductID(productID)iferr!nil{returnerr}product,err:s.repo.FindByID(ctx,pid)iferr!nil{returnerr}// 调聚合根方法扣库存iferr:product.DeductStock(qty);err!nil{returnerr}// 保存聚合iferr:s.repo.Save(ctx,product);err!nil{returnerr}// 库存低于阈值发布预警事件ifproduct.Stock()10{event:NewStockLowEvent(productID,10,product.Stock())iferr:s.publisher.Publish(ctx,event);err!nil{// 事件发布失败不影响主流程记录即可returnfmt.Errorf(扣库存成功但事件发布失败: %w,err)}}returnnil}应用服务只做编排不写业务规则。加载聚合、调方法、保存、发事件业务规则全在聚合根里。这样换一个应用场景业务规则不用重写。三、独家踩坑:贫血模型导致逻辑散落这个坑就是开篇那个库存系统的真实经历。最早的代码长这样结构体只有字段业务逻辑全在service。packageanemicimportfmt// Product 贫血模型只有字段没有方法typeProductstruct{IDstringNamestringPricefloat64StockintStatusstring}// ProductService 业务逻辑全在servicetypeProductServicestruct{}// DeductStock 扣库存逻辑散落在servicefunc(s*ProductService)DeductStock(p*Product,qtyint)error{// 规则散落在这里ifp.Status!on_sale{returnfmt.Errorf(商品未上架)}ifp.Stockqty{returnfmt.Errorf(库存不足)}p.Stock-qtyreturnnil}贫血模型的问题是业务规则没有归属。扣库存规则在ProductService但InventoryService也直接改p.StockAlertService也读p.Stock判断预警。任何代码都能绕过规则直接改字段规则形同虚设。那次超卖事故就是InventoryService里有一处直接p.Stock - qty没检查状态。引入聚合根后Stock字段不导出所有修改必须走DeductStock方法绕不开规则。packagedomainimportfmt// 对比: 充血模型强制走方法// 外部想改库存只能调DeductStock规则绕不开funcexampleUsage(){product,_:NewProduct(ProductID{value:p1},手机,Money{amount:99,currency:CNY})// product.stock 100 // 编译错误字段不导出改不了// 只能通过方法操作规则强制生效iferr:product.Restock(100);err!nil{fmt.Println(err)}iferr:product.PutOnSale();err!nil{fmt.Println(err)}iferr:product.DeductStock(5);err!nil{fmt.Println(err)}}经验是DDD要从充血模型开始。结构体字段默认不导出行为用方法暴露。业务规则集中在聚合根应用服务只做编排。这样规则有唯一归属改一处全生效。四、对比分析方案业务规则归属一致性保证复杂度可测试性适用场景贫血模型散落service弱低差简单CRUD充血模型聚合根方法强中好中等业务DDD分层聚合根上下文很强高很好复杂领域贫血模型开发快但业务规则散落规则容易被绕过适合简单CRUD。充血模型把规则收进聚合根一致性有保证适合业务规则明确的中等系统。完整DDD分层加限界上下文适合多个团队协作的复杂领域代价是设计成本高。总结与预告DDD的核心是让业务规则有归属。限界上下文划分领域边界聚合根保证业务不变量值对象封装不可变概念领域事件解耦上下文。从充血模型开始字段不导出行为用方法暴露业务规则集中在聚合根。贫血模型简单但规则易散落复杂业务一定要上聚合根。下一篇聊微服务通信对比gRPC、REST、消息队列三种方案怎么选。