领域驱动设计的源头书籍
关于DDD能找到的最早的一本书是《领域驱动设计 软件核心复杂性应对之道》,2003年Eric Evans 著,Martin Fowler作序。是的,你没看错,是差不多20年前的了。所以DDD不是什么新技术,而是老技术,但历久弥新,即使时代变迁,今天依然是经典。另一本《实现领域驱动设计》太厚了,转而学习同一作者[美] Vaughn Vernon 著的《领域驱动设计 精粹》。但感觉 Vaughn Vernon所理解的未必完全是 Eric Evans所表达的。 截图Eric Evans 的概念,后面再结合我自己的经验理解。
一、Layered Architecture分层定义
二、 时代变迁了哪些?
从上面的图示可以看出,20年前基本是桌面应用的时代,web应用也没有非常广泛,更不用说现在的云时代+移动时代。虽然现在依然流行桌面应用,但作为更广泛的业务应用系统,这里只讨论新时代下基于微服务架构风格如何应用DDD及20年前的不同点。
- 前后端分离。云+移动的时代,注定了前后端需要分离。单独看前端应用(例如Android App),也可以很复杂,除了界面展示的部分,也会有缓存、也会有远程API调用,也会有手机硬件设备(摄像头、指纹)等的基础设施访问。也是一个独立的完整的应用,也可以单独讨论DDD。但客户端应用架构风格上与桌面应用也差不多,不是这里讨论的主题。从更高的层面上,这里将frontend app与BFF app都看作是DDD分层的UI层。在Naresh Bhatia的文章中,认为SecurityWebService、TradingWebService 、MarketPriceListener 、ExchangeMessageListener 都属于UI层。我不太认同。我认为MarketPriceListener 、ExchangeMessageListener 属于Application Layer,而SecurityWebService、TradingWebService 一般在BFF里,属于UI层。是的,我将UI层从微服务架构风格的后端应用(下文简称“微服务应用”)里踢出去了,归到前端与BFF,微服务应用里只剩下三层。
- UI层不能再访问基础设施层,不能再访问领域层。参照图1各层访问关系,但其它各个箭头表达的访问关系仍然是成立的。
- 从分层定义来看,基础设施层,不再为用户界面层绘制屏幕组件。其它各层定义,依然成立。
三、 各层都包括了哪些?
1. UI Layer
- web app: html5 / css / js / img/ other static resource files
- 客户端应用:android App / iOS App, 微信小程序,支付宝小程序。
- 前端SDK:方便别家的APP集成的。
- BFF : Backend For Frontend , maybe based on node.js or other.
2. Application Layer:
可以协调Domain Service ,访问Domain Layer,作为IOC 或Bean Factory 装配各种Bean (一般由Spring 代劳了,但java Configuration 应当在此层)。 可以调用RPC ,监听消息队列。
- Application Service 。可以是各种rpc协议的接入服务。也可以是基于restful 的http接入服务(也可能是基于Spring MVC的输出 json的Controller)。
- Message Listener 实现类。接收MQ中间件的消息然后编排服务,协调相关资源,调用领域层对象。
- 部分定时跑批作业,如果没有独立部署的,也属于应用层。关于跑批作业的完成进度,也在这一层。
- Value Objects. 为了方便RPC传输的值对象。如果Domain Entity序列化性能也不差,则不一定需要VO,有时候Domain Entity不适合序列化传输(例如作为Aggregate Root ),则需要定义更合适的VO。
- Observer实现类: Business Lifecycle Callback Interface (Observable定义的接口)的具体实现类。一般负责监听并处理Business Lifecycle 的Domain Event。可以异步发送MQ,也可以同步调用RPC。Message Listener实现类是从外到内,从Application Layer到 Domain Layer;Observer实现类是从内到外,从Domain Layer到 Application Layer。
- Spring IOC, 是关于各处Bean的依赖注入、装配,是负责应用生命周期内各组件的依赖关系的。
3. Domain Layer:
领域层实际是Domain Business Model 领域业务模型,不是简单的数据模型,不能理解为E-R图中的E。这一层实际也是业务应用系统中六边形架构的核心。这个模块作为Artifact只需要Mock Repository接口,而不需要依赖于任何第三方库、第三方框架、中间件(Servlet容器、MQ中间件、DB服务器),就可以单独测试,并且验证当前Domain所有业务逻辑的正确性。如果某一个Class需要依赖于除了JDK之外的框架(log日志框架例外)才能编译通过或者单元测试,则这个Class很可能不属于Domain Layer。
Domain Entity :必须是富血模型,必须是业务模型里涉及的隐式或显示的概念,而且必须是有唯一标识(不一定是数字)。在这个Entity存续期内,其标识不能被修改。富血模型要求真正的面向对象,不只是有属性、setter与getter,还应该有对应的行为。应该尽可能的富血模型而不是贫血模型。如果某个function不依赖于外部而只依赖于对象内部的属性或者方法,则应该定义在Entity,不应该抽离到Utils也不应该抽离到Domain Service。
Value Objects: An object that represents some descriptive aspect of the domain, but has no conceptual identity is called a Value Object. A good example of a value object is Money. 必须是业务模型里涉及的隐式或显示的概念,一般没有明确的可追踪的ID标识。
Domain Event . 业务逻辑处理过程(Business Lifecycle)中发生的事件。不包括技术上的 Error or Exception or RuntimeException。如果一个外部MQ消息与当前领域模型有关,也可以包装成Domain Event,但一般在Application Layer优先考虑将MQ消息转化为Domain Entity / VO。
Observer Interface / Business Lifecycle Callback Interface . 可以在这一层定义观察者设计模式中的Observer接口,这些接口方法的形参可以有Domain Event,也可以没有Domain Event。通常Application层会实现具体的Observer,注入到这个领域层。领域层不能依赖于应用层,但可以通过回调接口的方式,间接调用应用层的代码。Application Layer也可以通过Callback Interface形成特定事件,然后发送到MQ中间件。
Domain Service :Some aspects of the domain are not easily mapped to objects. The best practice is to declare these as Domain Services. 一些不适合映射到领域对象里的过程,可以声明为领域服务。 Domain Service一般是无状态的单实例,可以用于协调涉及多个Entity的业务逻辑。所有的public的class / method / member必须是业务团队能理解的平时会说的名词或动词(统一语言)。如果某个方法名用的是技术名词而业务团队不能理解,则可能不适合放在Domain Model,而可能需要放在Infrastructure Layer或者 Application Layer。
Repository Interface: REPOSITORY应该让客户感觉到那些对象就好像驻留在内存中一样。FACTORY负责处理对象生命周期的开始,而REPOSITORY帮助管理生命周期的中间和结束。FACTORY和REPOSITORY具有完全不同的职责。FACTORY负责制造新对象,而REPOSITORY负责查找已有对象。这个接口的出参、入参必须是这层的Domain Entity或 Value Object,不能是DTO。Repository Interface 可以被Domain Service所调用,但不能被Domian Entity所调用。Repository Interface需要屏蔽基础设施的细节。领域业务模型决定了数据逻辑模型,数据逻辑模型决定了数据库物理模型。
4. Infrastructure Layer:
ORM(Object Relational Mapper)。
RPC Client. 微服务架构风格的应用都会存在大量的RPC调用。但每一个Domain都应该保持领域自治的特性,Domain Service不应该直接调用RPC。必要时可以由Application Service调用RPC转换为当前Domain的Enitity与VO递交给Domain Service。如果某个Domain Service 需要直接调用RPC,则应当考虑设计是否正确。在一次(Trace)调用链的所有RPC调用应当是符合DAG(以Application为节点的有向无环图),一旦违反DAG则应当重新考虑关于Domain Bounded Context 、 Application Service的定义、Application交互关系的设计是否正确。如果是基于Dubbo框架或者Hessian实现RPC,则相应的Skeleton属于这个RPC Client。如果是基于Thrift或者ProtoBuf 或者gRPC,则相应的IDL及生成的客户类库属于这个RPC Client。
MQ Helper. 与具体MQ技术细节相关的class 属于 Infrastructure Layer,但一般的Domain Layer 并不知道MQ的存在,中间是通过Applicarion Layer协调的。Application Layer可以 向Domain Layer注册观察者,监听特定的Domain Event,然后调用message producer发送。Application Layer可以实现Message Listener,注入到Infrastructure Layer的MessageConsumer, 监听MQ的消息,当onMessage时在Message Listener具体实现类中转换为Domian Entity / Value Object / Domain Event, 转交给Domain Service进行业务逻辑处理。
Servlet Container. 等负责底层协议的。
四、FAQ
1. Repository 属于Domain Layer 还是Infrastructure Layer?
阿里殷浩认为Repository的接口是在Domain层,但是实现类是在Infrastructure层。我也认为如此。Eric Evans 提到“REPOSITORY应该让客户感觉到那些对象就好像驻留在内存中一样”,所以Repository Interface 表达的仍然是当前Domain 的业务逻辑。但在具体实现时,这往往有些争议,因为要实现这个Repository Interface,implementation class 肯定要依赖于更底层的DB Driver或ORM,并且屏蔽技术细节。例如业务刚发展时性能需求不高,后来用户量激增,需要缓存了,单个关系型数据库不能满足了,于是用了Redis作缓存,部分数据可能又改为了MongoDB做NoSQL文档存储。假设Domain Model 做成一个单独的Artifact,为了保持其更纯粹的业务模型不被技术细节与底层设施所侵蚀,一般不希望这个artifact依赖于mybatis或Redis client。在六边形架构或洋葱圈架构,可以让这个infrastructure artifact 依赖于 domain model artifact。现在普遍流行MyBatis,可以认为mybatis mapper的xml配置文件属于Infrastructure Layer,是Repository Implementation。
2. 有哪些Artifact?构建时是怎样的依赖关系(maven depend)
- XxxApplication: denpend XxxDomain, XxxRepository, XxxRpcClient,XxxMQHelper
- XxxDomain: 无依赖,
- XxxRepository: depend mybatis, XxxDomain , Kkk-JDBC-Driver
- XxxRpcClient: depend rpc基础类库。
- XxxMQHelper: depend Kkk-MQ-Driver
3. 六边形架构关于输入输出的依赖
XxxDomian作为六边形架构的核心,不会直接依赖于外部。但
- 定义了Oberserver接口用于间接输出Domain Event到MQ,相当于定义了一个六边形架构的输出端口;
- 定义了Repository接口用于间接持久化到数据库或者缓存,相当于定义了一个六边形架构的输出端口;
- 定义了DomainService接口用于接受外部的rpc输入,或者接受MessageListener消息输入,相当于定义了一个六边形架构的输入端口。
具体的MessageListener实现类,Application Service 实现类,将外部的消息/rpc适配给到Domain Service,并协调相关资源装配相关流程。Observer具体实现类、Repository具体实现类作为适配器,由Applicaiton Layer注入到Domain Layer ,(当然实际Java项目有时候由Spring IOC代劳了一部分工作)。
Domain 不关注任何的技术框架与具体的技术细节,不关注任何的输入输出设施,只专注于业务逻辑,并且清楚的知道业务的生命周期,在流程的重要环节设置了接口,具有很大的可扩展性。如果人力有限,只能逐个Artifact开发,那么XxxDomain必须是第一个。
五、参考
- 《领域驱动设计 软件核心复杂性应对之道》[美]Eric Evans 著
- 《技术人的百宝黑皮书》阿里技术专家详解 DDD 系列(作者:殷浩)
- 《领域驱动设计 精粹》[美] Vaughn Vernon 著
- archfirst.org/domain-driven-design Naresh Bhatia
- 殷浩详解DDD系列 第三讲 - Repository模式