Appearance
领域驱动设计总览
领域驱动设计(Domain-Driven Design,DDD)解决的不是“项目目录怎样命名”,而是“复杂业务怎样被软件准确表达”。在一个交易系统里,订单、库存、支付、优惠、履约看起来都围绕一次购买发生,但它们关心的事实并不相同:订单关心购买承诺,库存关心可售数量,支付关心资金流转,履约关心商品交付。DDD 的价值,是把这些业务语义先讲清楚,再让代码、接口、数据库和消息围绕清楚的模型生长。
如果没有领域建模,复杂系统常会变成一张大表、一批通用服务和许多跨模块修改。短期看开发很快,长期看每次需求变更都像在拉一团缠住的线:改订单状态会碰到库存,改库存又影响发货,支付退款还要回头修订单。DDD 希望先把线头分出来,让每条业务线在自己的边界内保持清晰。
DDD 关注的两层问题
DDD 先做战略设计,再做战术设计。战略设计像城市规划,先决定商业区、仓储区、交通干线和行政边界;战术设计像建筑施工,在每个区域里设计房间、承重墙、管线和门禁。前者回答“模型边界在哪里”,后者回答“边界内部怎样写代码”。
战略设计的核心是子域、限界上下文和上下文映射。子域用来识别业务重要性,核心域承载竞争力,支撑域服务核心流程,通用域可以复用成熟方案。限界上下文为模型划定语义边界,同一个词跨出边界后含义可以变化。例如“商品”在商品中心是可售资料,在库存上下文是库存单位,在财务上下文是收入确认对象。上下文映射说明这些边界怎样协作,是直接调用接口、发布领域事件、引入防腐层,还是共享一小段内核模型。
战术设计负责把业务规则放回对象内部。实体表达有身份和生命周期的业务对象,值对象表达不可变的描述性概念,聚合保护必须同时成立的业务不变量,聚合根控制外部访问入口。领域服务处理不适合放进单个对象的领域规则,仓储隐藏持久化细节,领域事件表达已经发生的业务事实。它们不是装饰性名词,而是把业务规则从脚本式流程中拆出来,让代码能读出业务含义。
一条业务流程怎样进入模型
以“用户购买商品”为例,建模不要从数据库表开始,而要从业务事实开始。用户提交订单后,订单已创建;支付完成后,订单已支付;库存确认后,库存已锁定;仓库发货后,包裹已发出。每个“已发生”的关键事实都可以成为领域事件,它们串起来就是业务时间线。
沿着时间线继续观察,会看到不同边界的职责。订单上下文记录购买意图和订单生命周期,不应该直接计算仓库实物库存;库存上下文维护可售、锁定、释放这些库存规则,不应该知道订单页面怎样展示;支付上下文处理支付单、交易流水和退款状态,不应该把订单明细复制成自己的核心模型。边界清楚后,服务拆分、模块拆分和数据库设计才有稳定依据。
进入上下文内部后,再识别聚合。订单聚合可以包含订单项和金额,但优惠券账户、库存记录、支付流水不应塞进同一个聚合,因为它们有不同生命周期和一致性要求。订单支付成功后,可以通过领域事件通知库存和履约,而不是在订单聚合里直接修改所有对象。这样做不是为了追求形式,而是为了避免一个聚合越来越大,最终变成无法维护的全局对象。
适用边界
DDD 更适合规则密集、语义容易混淆、生命周期较长的核心系统,例如交易、结算、定价、风控、库存、履约、保险核保、供应链计划、医疗诊疗等。它们的难点不是接口数量,而是业务概念之间的边界、状态变化和规则约束。
简单录入、短期原型、纯展示型后台、规则很少的 CRUD 系统,不需要完整引入 DDD。此时清晰分层、良好的命名、必要的模块边界已经足够。把所有类强行命名成实体、值对象、领域服务,不会让系统自动变好,反而会增加理解成本。
判断是否需要 DDD,可以看几个工程信号:业务人员解释需求时经常强调“这里的订单不是那个订单”;同一个状态被多个模块随意修改;核心规则散落在控制器、服务类、SQL、定时任务和消息消费者里;开发者必须翻大量历史代码才能判断一个业务动作能否执行。出现这些信号时,应先补领域建模,再讨论微服务或数据库重构。
阅读路径
先读 模型详细说明,建立统一语言、限界上下文、实体、值对象、聚合、领域事件之间的关系。再读 聚合根,重点理解一致性边界和外部访问入口。随后阅读 事件风暴 和 实践指南,把业务讨论转换成可落地的模型。需要画图表达模型时,转到 UML 建模 模块,用类图、时序图和活动图补充沟通材料。
如果当前系统主要问题是业务边界不清,优先研究子域、限界上下文和上下文映射。如果主要问题是规则散乱,优先研究聚合、领域服务、应用层编排和基础设施隔离。如果主要问题是服务拆分混乱,先回到业务模型,不要直接从表、接口或团队组织切服务。
落地检查
一个有效的 DDD 模型,读代码时能看到业务语言,而不是只看到技术动作。payOrder 应该表达支付订单时要校验什么规则、改变什么状态、产生什么事实,而不是把所有判断散在应用服务和 SQL 里。限界上下文能解释模型为什么不同,聚合能说明哪些规则必须在一个事务里成立,领域事件能表达边界之间已经发生的事实。
DDD 落地后,团队讨论需求时会自然追问:这个词属于哪个上下文;这个状态由谁负责改变;这条规则是不是聚合内不变量;跨上下文协作需要同步接口还是事件;失败后是回滚、补偿还是等待重试。能回答这些问题,说明模型已经开始承担架构责任。
