谈论到 DDD,我们会聊事件风暴,会聊限界上下文,会聊六边形架构,会聊实体值对象。这些概念各不相同,相关的概念也很不一样,但都属于 DDD 的范畴。见过了很多 DDD 的讨论和工作坊,我发现大家唇枪舌剑无法达成一致,往往是因为各自脑中的问题并不相同。
我尝试在软件设计领域,将这些问题划分到几个相互独立的范畴,这可以帮助我和其他人讨论,在明确范围内可以更好的交流。
一种比较经典的方式是划分为战略设计和战术设计。由于领域模型设计复杂度也很高,所以我又把领域模型设计从战术设计中划分出来,形成单独的范畴,以便更好的讨论。
下面我将讨论这三个范畴的概念和方法。
DDD 战略设计
在这个范畴里,主要讨论目标是复杂的业务需求。
有多复杂呢?可能需要多个团队分工合作,或者一个团队分阶段开发,需要被设计成多个独立部署运行的服务,会有多个代码库。
这个范畴可以有很多名字,比如 DDD 战略设计、进程间架构、微服务架构设计等。
在这个范畴里讨论的主要问题是,如何将这个复杂的业务需求合理的分成多个部分,从而分而治之。
为什么要分成多个部分?因为解决复杂问题的一个有效方法是将其分解为多个相对简单的问题,然后分别解决。如果不进行分解,这个复杂问题往往会让我们在解决过程中陷入困境,就算设计出了解决方案,也往往由于解决方案过于复杂导致团队的认知超载。
划分方法
既然战略设计需要将整个业务需求分成多个部分,那么如何找到用于划分的接缝呢?
我看到行业里有这样一些方法:
- 限界上下文
在《领域驱动设计》中,Eric 提出了限界上下文。从领域模型设计的角度,为了让模型保持完整独立和清晰,需要识别出限界上下文,让其作为模型的边界。在书中并没有完善的识别方法,更多的是提出一些概念。限界上下文往往被用来辅助判断接缝的正确性。
在一个限界上下文中,领域知识是相对完整的。
- 核心域
在《领域驱动设计》中,Eric 提出了精炼及核心域。在模型中识别出最有价值的核心域,将其独立出来。
由于只提到了核心域,所以这也不是一个完整的划分的方法。我曾在如何划分限界上下文博客中基于此方法上提出了一种分解问题域的方法。
- 事件风暴工作坊
事件风暴工作坊可能是最早用来指导划分限界上下文的方法。
对前一步(事件风暴)产生的聚合进行分组,通过业务的内聚性和关联度划分边界,结合限界上下文的定义进行判断,并给出上下文名称。
——[服务化设计阶段路径方案]
但是「业务的内聚性和关联度」着实不是一个好的划分依据。而事件风暴的创始人 Alberto 曾经提出过通过关键事件识别不同的阶段进而识别限界上下文的方法,看上去是一个更加靠谱的方法。
- 8X Flow
8X Flow 中提出了一套相对完整的划分方法。首先定义「业务」和「领域」,然后将「业务」和「领域」划分开来,接着基于合同将业务划分成了不同的上下文,最终完成了划分。
- 现代企业架构白皮书
现代企业架构白皮书提出通过职责类型划分。流转类识别不同的业务流程阶段,规格类提取业务规则,视图类专为统计报表而存在,配置类提供配置工具。
重新思考
我也尝试过一些其他的划分方法,比如通过时间阶段划分,通过使用者不同划分,通过使用场景不同划分,通过变化频率不同划分。这些方法和上面的一些方法都有些相似。
不好的划分方法可能会导致分布式单体:每次变化不得不修改多个服务、每次部署必须同时部署多个服务,服务之间有非常多的通信,同一个团队管理着多个服务,服务之间共享数据库、同样的代码和模型。
也许我们可以总结出一些原则,来帮助我们验证划分是否合理。比如高内聚低耦合,比如服务有明确的边界且能自治,可以独立演进,比如尽可能减少对于其他服务的依赖。
DDD 战术设计
在战略层面划分好了服务后,我们来看看一个服务内部。
在这个范畴里,主要讨论在一个服务内部,如何划分和组织代码。
和上一节类似,在代码也有不同的职责;和上一节不同,对于代码层面的划分,已经有相对成熟的方法。
这个范畴可以有很多名字,比如 DDD 战术设计、进程内架构、分层架构等。
需要指出的是,在一个服务内部,如果领域模型足够复杂,在分离领域逻辑和技术实现细节前,也需要先按照模块进行一次划分,然后再按上述的领域逻辑和技术实现细节的方式划分。相关讨论可以参见前缀分包 vs 后缀分包。
划分方法
- 《领域驱动设计》中的分层架构
Eric 在 2003 年提出的分层架构。和传统的展示层+业务逻辑层+数据访问层的三层架构相比多了一层,主要区别是将业务逻辑层分成了应用层和领域层。
图片引自《领域驱动设计》第 4 章
其中「应用层」这个概念,也指明了它和领域层的区别:领域层专注表达领域概念,而应用层则在领域层之上,加入了诸如持久化概念和事务概念等软件的典型概念,对外提供了满足具体场景的功能。展示层则在应用层功能之上,定义了和外部系统通信的具体形式。
这里也将数据访问层变成了基础设施层。基础设施层为其他层提供支撑其概念的具体技术实现。
- 六边形架构
2005 年六边形架构(翻译)又称端口和适配器架构,从设计模式的视角将代码划分成了负责业务逻辑的「应用」和负责同外部系统交互的「适配器」。
图片引自《六边形架构》
在 2013 的 IDDD 中 Vaughn 将六边形架构和 DDD 进行了结合,把「应用」又细分成了「应用程序」和「领域模型」。
图片引自《实现领域驱动设计》第 4 章
2008 年的洋葱架构也是类似的。
六边形架构从另外一个角度审视了一个理想架构,并将领域层放在中心,凸显其核心地位。
- 整洁架构
Uncle Bob 在 2012 提出了整洁架构,一般来说我们认为整洁架构的四层(四圈)和 IDDD 的六边形架构基本是对应的,只是整洁架构将适配器划分成了和框架耦合的「Frameworks & Drivers」层和负责内外层数据转换的「Interface Adapters」层。
图片引自《整洁架构》
整洁架构也用「用例」来描述业务实体之外的一层,对应于「应用层」,更明确的指明了这层的职责是实现各个用例。
比较有趣的是,整洁架构把 Gateway 接口放到了领域层之外的「用例层」。这使得领域层只关注于当前上下文的逻辑,而让用例层负责和其他上下文/资源库的协调和编排。
整洁架构也讨论了如何处理框架和架构的关系。
- 清晰架构
2017 年更有集 DDD、洋葱架构、整洁架构、CQRS 于一体的清晰架构出现。
重新思考
以上的架构,指导每一个具体的业务功能分解来说是非常够用的。然而在一个真实的项目中,除了每个具体功能的分层,其实还有一些对于平台和框架的配置,这些其实要和每个业务功能的代码有所区分,从代码结构上独立出来。
另外,每一层都会有一些可以复用的代码。比如领域层的基础的业务异常,应用层的事务处理,适配器层的 HTTP 客户端。这些不只用于单个模块或者单个服务,也可以用于多个服务;有些已经有三方工具,有些需要我们自己定义和封装。
我看到很多项目对于以上两类代码并没有区分,而是把一切不属于其他层的代码都放到了基础设施层。让可怜的基础设施层逐渐变成了垃圾桶。
领域模型设计
在战术层面划分好架构后,我们来看看位于核心的领域模型。
在这个范畴里,主要讨论基于面向对象技术,如何用领域模型来表达业务概念。
为什么要使用领域模型这种模式,而不是用 Service+ 数据模型的模式呢?如果复杂的业务逻辑采用数据模型这种模式,那么 Service 里会存在大量的复杂的逻辑,代码是很难维护的。而领域模型充分利用了面向对象技术的优势,将复杂度转变为职责明确的组件组合,让各个组件相对简单,来降低认知负载,提升可维护性。这就是设计的力量。
那为什么用面向对象技术呢?面向对象思想更加符合我们认知复杂问题的方式,并且现代编程语言都普遍支持面向对象,所以 DDD 选择了面向对象技术。
关注点分离模式
在这个范畴里,主要还是使用《领域驱动设计》中的模式。我们以关注点分离的角度,来解析这些模式。
- 领域对象的生命周期类型
从生命周期的角度,「领域对象」分为这样几个类型:
- 和应用生命周期一致,应用启动时被创建出来,应用关闭时才销毁。比如《领域驱动设计》5.4.1 中的「资金转账」。
- 在业务过程中被创建,会被保留一段时间,不随着应用关闭销毁。比如电商系统中的「订单」。
- 在业务过程中被创建,在使用完成后即被销毁。比如一些在对象之间传递的参数对象。
而在《领域驱动设计》的第 5 章,Eric 也将领域对象划分为了实体、值对象、领域服务这三个重要模式。这三个模式和生命周期是如何对应的呢?
对于类型1,和应用生命周期一致,就是领域服务这种模式。对于类型2,在业务过程中被创建,会被保留一段时间,对应于实体和值对象。而对于类型3,在业务过程中被创建随即被销毁,对应于值对象。
VALUE OBJECT 经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。
——《领域驱动设计》 5.3 值对象
- 分离领域对象的创建、查询、保存和使用
从生命周期角度,对于这三类领域对象的创建逻辑,可以使用 Factory 模式,将其封装在 Factory 中。对于类型 2 的领域对象的保留及之后的查询,可以使用 Repository 模式,将其模拟成一个集合从而进行存取操作。
Eric 把 Factory 和 Repository 被归为「支持对象」,以和其他用于表示模型的领域对象分开。
- 分离函数和命令
使用无副作用的函数模式,把没有副作用的查询逻辑提取出来,成为无副作用的函数,而让有副作用的命令尽可能简单。
基于同样的理由,我也在考虑将有 IO 操作的逻辑提取出来,直接让应用层调用,而不是和其他业务逻辑组合。
- 分离领域中的算法
使用 Strategy 模式,把业务逻辑中的变化点放到策略对象中,让不同的实现可以互换,从而实现关注点分离。
- 分离领域中的规则
使用 Specification 模式,将领域中用于判断是非的业务规则放到规格对象中。
- 分离做什么和怎么做
采用 Intention-Revealing Interface 和 Cohesive Mechanism 模式,把「做什么」和「怎么做」分离。让释意接口专注于表明意图,方便调用方使用;让内聚机制封装实现细节,在释意接口背后解决问题。
重新思考
我发现在 OO BootCamp 中得到的模型往往无法直接用于真实项目中,这让我用新的角度重新学习和思考了领域模型。
在实际项目中,设计者往往过早陷入对于一些具体模式的识别,比如实体、聚合、领域服务,而忽略了如何设计一个可以表达领域概念的模型。我们应该基于领域概念设计领域模型,然后再采用合适的模式降低领域模型的复杂度,进一步增加领域模型的表达能力。
很多项目虽然也使用了以领域为核心的架构,但是设计者仍然是数据模型/贫血模型的思考方式,把大量领域逻辑放置在了万能的 Service 中,让领域概念隐藏在了冗长的过程代码中,丝毫没有享受到 DDD 带来的收益。
徐昊在极客时间的《如何落地业务建模》课程中说 DDD 至少可以指代一种建模法,一种协同工作方式和一种价值观。而我在这里着重讨论了软件设计相关的建模法。
Eric 在《领域驱动设计》一书中提出,软件设计应该以领域为中心,而不是技术问题。
软件的核心是其为用户解决领域相关的问题的能力。
——《领域驱动设计》 第一部分
在学习了让我们眼花缭乱的众多方法后,我们重新回到 DDD 的初衷,重新审视软件设计和 DDD 之间的关系,让 DDD 帮助我们提升软件设计能力。