《实现领域驱动设计》(Implementing Domain-Driven Design)是Vaughn Vernon写的一本关于领域驱动设计(DDDDomain-Driven Design)的经典书籍出版于2013年是继Eric Evans的《领域驱动设计》(蓝皮书)之后,又一本DDD领域的重要著作被称为DDD的"红皮书"。

相比Eric Evans的原著更偏重思想和概念这本书更注重实践和实现讲了很多具体的技术和方法包括怎么设计聚合怎么实现仓储怎么用事件溯源怎么和,REST消息等技术结合等等能帮开发者把DDD落地到实际项目中非常实用。

最近我重读了这本书梳理了核心观点本文用三分钟的时间带大家读懂这本书的核心思想和实践方法包括DDD的核心概念战略设计战术设计以及,实现技巧等等希望能帮大家快速理解DDD把DDD用到自己的项目中。

一、DDD的核心思想

先说说DDD的核心思想这是理解DDD的基础。

DDD的核心思想是让软件的设计和业务领域保持一致也就是说软件的代码结构对象模型应该和业务领域的概念规则流程对应起来让技术人员和,业务人员能用同一套语言交流减少沟通成本也让软件更能反映业务的本质更易维护更易扩展。

DDD强调领域是软件的核心技术只是实现领域的手段不要让技术细节淹没了业务领域的概念很多项目的问题就是技术人员只关注技术不关注业务导致软件和,业务脱节业务人员看不懂代码技术人员不理解业务沟通成本高软件也难维护难扩展DDD就是为了解决这个问题。

DDD分为战略设计战术设计两个部分战略设计是从宏观上划分领域边界定义上下文和子领域战术设计是从微观上设计领域模型的具体元素,比如实体值对象聚合领域事件等等下面分别介绍。

二、战略设计:划分领域边界

战略设计是DDD的重要部分也是很多人容易忽略的部分很多人学DDD只关注战术设计的实体值对象聚合等等,但是战略设计才是DDD的前提和基础,如果领域边界划分不对战术设计再好也没用。

1. 统一语言(Ubiquitous Language)

统一语言是DDD的基础也是核心要求统一语言是指技术人员和业务人员在交流文档代码中使用同一套一致的业务术语,比如业务上叫"订单"代码里就叫Order不要叫什么OrderInfoOrderDTOOrderEntity等等乱七八糟的名字保持一致。

统一语言能减少沟通成本避免误解也能让代码更能反映业务的本质建立统一语言需要技术人员和业务人员一起讨论梳理业务概念定义术语,并且在整个项目中坚持使用不断完善这是一个持续的过程不是一次性的工作。

2. 限界上下文(Bounded Context)

限界上下文是DDD战略设计的核心概念限界上下文是指一个统一语言适用的边界在这个边界内所有术语的含义是,一致的超出这个边界术语的含义可能就不一样了。

比如"产品"这个词在销售上下文里指的是可以卖的商品有价格库存等等属性在研发上下文里指的是正在开发的产品有需求版本迭代等等属性两个上下文里的"产品"含义不一样,所以要划分不同的限界上下文每个上下文里的模型是独立的不要混在一起。

限界上下文的划分是DDD的关键也是难点划分得好系统就清晰易维护划分得不好系统就混乱难维护划分限界上下文的依据是业务的内聚性和边界把业务上紧密相关的概念放在一个上下文里把业务上不同的概念分开通常一个限界上下文对应一个业务领域,或者一个业务子系统。

限界上下文和微服务的关系很密切通常一个限界上下文可以对应一个微服务,但是不是绝对的一个上下文也可以是一个模块,或者一个应用关键是边界清晰模型独立。

3. 上下文映射(Context Map)

上下文映射是描述不同限界上下文之间,关系的工具,因为系统里有多个限界上下文它们之间,需要协作,所以要定义它们之间,的关系和集成方式。

常见的上下文关系模式有:

  • 共享内核(Shared Kernel):两个上下文共享一部分模型和代码减少重复,但是耦合也高要谨慎使用。
  • 客户-供应商(Customer-Supplier):一个上下文是,另一个的客户依赖另一个提供的服务供应商要考虑客户的需求。
  • 遵奉者(Conformist):客户完全遵从供应商的模型没有话语权供应商怎么给客户就怎么用集成简单,但是受供应商限制。
  • 防腐层(Anti-Corruption LayerACL):客户在自己和供应商之间,加一层防腐层把供应商的模型转换成自己的模型保护自己的领域模型不被供应商的模型污染这是很常用的模式特别是集成外部系统的时候。
  • 开放主机服务(Open Host Service):供应商提供一个开放的服务接口(比如REST API)让客户能方便地集成定义清晰的协议和契约。
  • 发布语言(Published Language):上下文之间,用一种公共的语言(比如XMLJSON格式)交换数据大家都遵守这个格式。
  • 另谋他路(Separate Way):两个上下文没有关系不集成各自独立,如果集成的成本太高,或者没有必要就选择这种方式。

上下文映射能帮我们理清系统中各个上下文之间,的关系选择合适的集成方式避免耦合过紧,或者集成混乱是战略设计的重要工具。

4. 子领域(Subdomain)

子领域是从业务的角度对整个业务领域的划分和限界上下文是从技术设计的角度划分不同子领域是业务上的划分通常一个子领域对应一个限界上下文,但是也可能一个子领域包含多个上下文,或者一个上下文跨多个子领域理想情况是一一对应。

子领域分为三类:

  • 核心域(Core Domain):业务的核心是公司竞争力的关键是需要重点投入精心设计的部分,比如电商的订单和商品领域就是核心域。
  • 支撑域(Supporting Domain):为核心域提供支撑的子领域不是核心,但是必需,比如电商的库存物流领域就是支撑域。
  • 通用域(Generic Domain):通用的很多公司都有的子领域没有差异化可以用第三方解决方案,比如认证授权支付通知等等就是通用域。

划分子领域的意义是合理分配资源核心域要重点投入用最好的团队最精心的设计支撑域次之通用域尽量用现成的方案不要自己造轮子这样能把资源用在刀刃上提升公司的竞争力。

三、战术设计:领域模型的构建块

战略设计划分好边界之后,就是战术设计在每个限界上下文内部构建领域模型DDD提供了一套战术设计的构建块包括实体值对象聚合领域服务领域事件仓储工厂等等下面分别介绍。

1. 实体(Entity)

实体是有唯一标识的领域对象它的身份由标识决定而不是由属性决定,比如用户是实体每个用户有唯一的用户ID两个用户,即使名字年龄都一样,只要ID不一样就是不同的用户。

实体的特点是有生命周期会变化属性会改变,但是标识不变实体封装了自己的状态和行为通过方法来改变状态保证业务规则不被破坏不要把实体做成,只有getter/setter的贫血模型要让实体有丰富的行为体现业务规则。

2. 值对象(Value Object)

值对象是没有唯一标识的领域对象它的身份由属性值决定两个值对象,只要所有属性都一样就是相等的,比如地址是值对象两个地址,只要省市区街道都一样就是同一个地址不需要ID。

值对象的特点是不可变(Immutable)创建之后,就不能修改要修改的话,创建新的值对象替换旧的不可变性让值对象更安全更易共享也避免了很多并发问题值对象应该尽量多用能用值对象的地方就不要用实体简化模型。

值对象还可以封装业务规则,比如金额值对象可以封装货币和数值保证金额不为负等等比用原始类型(比如BigDecimal)更能体现业务含义也更安全。

3. 聚合(Aggregate)

聚合是DDD战术设计中最核心也最难的概念聚合是一组相关的实体和值对象的集合它们作为一个整体被修改和访问有一个聚合根(Aggregate Root)作为对外的唯一入口外部只能通过聚合根来访问和修改聚合内部的对象不能直接访问聚合内部的其他对象。

比如订单是一个聚合订单(Order)是聚合根订单项(OrderItem)是聚合内部的实体外部不能直接修改订单项只能通过订单的方法(比如addItemremoveItemchangeItemQuantity)来修改订单项这样能保证订单的业务规则(比如总金额计算库存检查等等)不被破坏。

聚合的核心原则是一致性边界在一个聚合内部所有的业务规则必须在每次修改后保持一致,也就是说聚合是事务的一致性边界一个事务只修改一个聚合不要在一个事务里修改多个聚合这样能保证一致性也能提升性能和可扩展性。

聚合的设计原则:

  • 聚合要小:尽量设计小的聚合不要把很多不相关的对象都放在,一个聚合里聚合越小性能越好并发冲突越少也越易维护。
  • 通过ID引用其他聚合:聚合之间,不要直接对象引用要通过ID引用,比如订单聚合引用用户聚合不要在订单里放User对象要放userId这样能解耦聚合也能避免加载整个对象图提升性能。
  • 一个事务只修改一个聚合:这是聚合的重要原则不要在一个事务里修改多个聚合,如果需要多个聚合协作用最终一致性(领域事件异步处理)而不是强一致性事务。

聚合的设计是DDD的难点也是关键设计得好系统就清晰一致易扩展设计得不好系统就混乱难维护需要结合业务场景仔细思考和权衡。

4. 领域服务(Domain Service)

领域服务是承载不属于任何实体或值对象的领域逻辑的对象有些业务逻辑涉及多个实体,或者值对象不适合放在某个实体里这时候就放在领域服务里。

比如转账操作涉及两个账户实体(转出账户和转入账户)不适合放在某个账户实体里就可以放在,转账领域服务里由领域服务协调两个账户完成转账。

领域服务的特点是无状态(没有自己的状态)只有行为用来协调多个领域对象完成某个业务操作注意不要把所有逻辑都放在领域服务里导致贫血模型能放在实体或值对象里的逻辑就放在实体或值对象里,只有确实不属于任何对象的逻辑才放在领域服务里。

5. 领域事件(Domain Event)

领域事件是领域中发生的有业务意义的事情,比如订单已创建用户已注册商品已下架等等领域事件是DDD中非常重要的概念能实现聚合之间,的解耦和最终一致性也能实现业务的可追溯和审计。

领域事件的特点:

  • 是过去式:领域事件是已经发生的事情,所以命名用过去式,比如OrderCreatedUserRegistered。
  • 不可变:事件一旦发生就不能修改是事实的记录。
  • 包含必要信息:事件要包含处理这个事件需要的信息,比如订单ID用户ID时间等等。

领域事件的用途:

  • 聚合间解耦:一个聚合发生了某个事情发布事件其他聚合订阅事件做相应的处理这样聚合之间,没有直接依赖解耦了。
  • 最终一致性:因为一个事务只修改一个聚合其他聚合的修改通过事件异步处理实现最终一致性而不是强一致性这是分布式系统的常用方式。
  • 事件溯源(Event Sourcing):不存聚合的当前状态而是存所有发生的事件需要当前状态的时候,把事件从头回放一遍得到当前状态这就是事件溯源能完整记录业务历史也能方便调试和回溯,但是实现复杂要谨慎使用。

领域事件是DDD中非常强大的工具,但是也不要滥用,只有真正有业务意义的事情才发布事件不要什么都发事件导致系统复杂难维护。

6. 仓储(Repository)

仓储是负责持久化和读取聚合的对象它提供类似集合的接口让领域层像操作内存集合一样操作聚合而不用关心底层的持久化细节(比如用什么数据库SQL怎么写等等)。

仓储的特点:

  • 针对聚合:每个聚合对应一个仓储,比如OrderRepositoryUserRepository不要给每个实体都建仓储,只有聚合根才有仓储。
  • 接口在领域层实现在基础设施层:仓储的接口定义在领域层(比如OrderRepository接口)具体实现在基础设施层(比如JdbcOrderRepositoryJpaOrderRepository)这样领域层不依赖基础设施符合依赖倒置原则。
  • 提供基本的增删改查:仓储提供savefindByIdfindAlldelete等基本方法也可以根据业务需要提供特定的查询方法,但是不要把仓储做成万能的查询工具复杂的查询可以用CQRS的查询模型单独处理。

仓储的作用是解耦领域层和持久化层让领域模型不依赖具体的数据库技术也让持久化细节不污染领域模型是DDD分层架构的重要部分。

7. 工厂(Factory)

工厂是负责创建复杂聚合或实体的对象当聚合的创建过程比较复杂(比如需要初始化很多属性创建关联对象校验规则等等)不适合放在构造函数里就用工厂来创建封装创建的复杂性。

工厂的特点:

  • 封装创建逻辑:把复杂的创建逻辑封装在工厂里调用方不用关心创建的细节只需要调用工厂的方法就能得到创建好的对象。
  • 保证对象完整性:工厂在创建对象的时候,会做必要的校验保证创建出来的对象是完整的合法的符合业务规则的。
  • 可以是方法也可以是类:简单的工厂可以是聚合根的一个静态方法,或者构造函数复杂的工厂单独做一个工厂类,或者工厂接口+ 实现。

工厂和仓储的区别是工厂负责创建新的对象(内存中的新对象)仓储负责持久化和读取已经存在的对象(从数据库读取,或者保存到数据库)两者职责不同不要混淆。

四、DDD的分层架构

DDD通常采用分层架构把系统分为几个层每个层有明确的职责层之间,有清晰的依赖关系常见的DDD分层架构是四层架构:

  1. 用户界面层(Interface Layer / Presentation Layer):负责和用户交互接收用户请求返回响应,比如ControllerREST API页面等等这一层不包含业务逻辑只负责请求的接收参数的校验和响应的组装。
  2. 应用层(Application Layer):负责协调领域对象完成业务用例是业务流程的编排者,比如下单用例应用层会协调用户聚合商品聚合订单聚合仓储等等完成下单流程应用层不包含业务规则业务规则在领域层应用层只负责编排和协调。
  3. 领域层(Domain Layer):负责核心业务逻辑和领域模型是系统的核心包括实体值对象聚合领域服务领域事件仓储接口等等这一层不依赖其他层是最独立的最稳定的层。
  4. 基础设施层(Infrastructure Layer):负责提供技术支撑,比如数据库访问(仓储实现)消息队列缓存第三方服务集成等等这一层实现领域层定义的接口(比如仓储接口)为其他层提供技术服务。

依赖关系是用户界面层→应用层→领域层←基础设施层,也就是说上面的层依赖下面的层领域层是核心不依赖任何其他层基础设施层依赖领域层(实现领域层的接口)符合依赖倒置原则。

这种分层架构能让系统职责清晰耦合降低易维护易扩展也能让领域模型保持独立不被技术细节污染是DDD的推荐架构。

五、DDD的实践建议

最后分享一些DDD的实践建议帮大家更好地落地DDD。

建议1:不要为了DDD而DDD

DDD不是银弹不是所有项目都适合用DDDDDD适合业务复杂的项目(比如电商金融ERP等等)如果是简单的CRUD项目业务逻辑很简单用DDD反而会增加复杂度得不偿失,所以要根据项目的业务复杂度选择是否用DDD不要为了用DDD而用DDD。

建议2:从战略设计开始不要一上来就搞战术设计

很多人学DDD一上来就搞实体值对象聚合这些战术设计,但是忽略了战略设计这是不对的战略设计是前提要先划分好限界上下文子领域上下文映射理清业务边界再在每个上下文内部做战术设计,否则战术设计再好也是在错误的边界里做没有意义。

建议3:聚合要小不要设计大聚合

聚合的设计是难点很多人容易设计大聚合把很多对象都放在一个聚合里导致聚合庞大复杂性能差并发冲突多难维护要尽量设计小的聚合只把真正需要强一致性的对象放在一个聚合里其他的用ID引用通过事件实现最终一致性这样聚合小性能好易维护。

建议4:统一语言要坚持不要半途而废

统一语言是DDD的基础,但是也是最难坚持的很多项目开始的时候,还注意统一语言后来就慢慢不坚持了代码里又出现各种乱七八糟的命名和业务术语不一致DDD的效果就大打折扣,所以统一语言要整个团队一起坚持在交流文档代码中都使用统一的术语持续完善这是一个长期的过程。

建议5:渐进式落地不要一次性大改

DDD的落地不要一次性大改整个系统风险太高要渐进式落地先从核心域开始用DDD的方式设计和实现核心域验证效果没问题再逐步推广到其他子领域老的代码可以慢慢重构迁移不要急于求成这样风险小也能让团队慢慢适应DDD的思维和方式。

六、写在最后

以上就是《实现领域驱动设计》这本书的核心观点梳理包括DDD的核心思想战略设计(统一语言限界上下文上下文映射子领域)战术设计(实体值对象聚合领域服务领域事件仓储工厂)分层架构以及实践建议希望能帮大家三分钟读懂这本书的核心内容。

DDD是一种设计思想也是一种开发方法论它的核心是让软件和业务保持一致用统一的语言交流用清晰的边界划分用丰富的领域模型体现业务规则,虽然DDD有一定的学习成本和复杂度,但是对于业务复杂的系统来说能带来很大的收益让系统更易维护更易扩展更能适应业务的变化。

《实现领域驱动设计》这本书是DDD实践的经典书籍比Eric Evans的原著更注重实践和实现讲了很多具体的技术和,方法适合想把DDD落地的开发者阅读推荐大家有时间读一读原书会有更深入的理解和收获。

最后用一句话结束这篇文章:"DDD不是银弹,但是对于业务复杂的系统它是非常好的设计思想和方法能帮我们构建出更能反映业务本质更易维护更易扩展的软件系统。"

愿大家都能理解DDD用好DDD构建出优秀的软件系统。