我在《解构领域驱动设计》一书中分析了软件复杂度的成因,一曰规模,一曰结构,还有一个则是变化的影响。规模与结构存在一定的矛盾关系:解决规模复杂度的有效方法为“分而治之”,一旦系统被分解为多个更为细小的软件元素,结构复杂度就会增加。结构与变化之间存在互相影响的关系:如果结构控制不合理,变化带来的影响就会更强,使得系统更加复杂。
认真分析结构和变化对系统复杂度的影响,一个关键是对依赖的控制。当我们对系统进行分解时,依赖会成为我们无法绕开的问题,它是技术债的重要组成部分,是不可避免的。如果没有控制好依赖,系统的架构就会随着时间的推移不可避免地腐化下去,如人不可避免的老去。
要合理控制依赖,只有两个可行的思路:
- 从多到少:减少依赖而非彻底消除依赖,其核心原理是做好职责的合理分配
- 从强到弱:如果依赖不可避免,则要想办法降低依赖,其核心原理是封装与抽象
减少依赖数量
领域驱动设计通过引入限界上下文和聚合,在战略层次和战术层次分别提供了减少依赖的控制手法。
限界上下文
我在《解构领域驱动设计》书中总结了限界上下文的两个特征:
- 限界上下文是领域模型的知识语境
- 限界上下文是业务能力的纵向切分
此两大特征既充分阐释了限界上下文的本质,又提供了减少依赖的思路。
领域模型的知识语境
限界上下文提供了领域概念的知识边界,在特定的上下文之下,领域概念体现的是一种局部的全貌。所谓“局部的全貌”听起来颇为矛盾,然而就观察者而言,在你所处的“上下文”边界内,由于你并不关心其他上下文的知识,因而你看到的虽然只是“局部”,却可以理解为这是你需要的“全貌”。
譬如在一家软件企业,假设一个员工作为我们要了解的全貌,在不同部门看到的却只是各自的局部:
- 人力资源部门:教育背景、现有角色、岗位、职务、考勤
- 财务部门:薪资、社保
- 项目管理部门:技能水平、所属团队、团队角色
以财务部门为例,我并不需要知道该员工技能水平如何,也不需要知道他在哪一个团队,只需要知道该员工的薪资构成,然后按照企业规章核算工资并按时发放即可。员工薪资与社保等信息就在此时构成了在财务上下文下财务人员关心的全貌。
这一设计的好处在于引入了领域知识的控制边界,倘若分配合理,就能减少软件元素之间不必要的依赖关系。这也是限界上下文与模块之间的不同之处。
如果以业务模块对系统进行分解,一种直观的设计方案是单独分解出一个员工模块,然后将该员工的所有属性与行为(构成了领域知识)都分配给员工模块。当财务部门进行工资核算与支付时,需要从员工模块中获取它与薪资、社保相关的领域知识,带来了财务模块与员工模块之间的依赖关系。
限界上下文则不同,由于员工与财务相关的领域知识都根据上下文分配给了需要关注这些领域知识的财务上下文,在核算工资与支付工资时,财务上下文就无需求助于员工上下文了。这是让依赖减少的最佳模式。
业务能力的纵向切分
限界上下文与模块之间的不同之处,还在于限界上下文不止限于封装了领域知识。它是对业务能力的纵向切分,如此切分出来的每一块,都是相对独立而完整的。准确的说法,就是先根据领域维度对整个系统进行纵向切分,然后再到限界上下文内部,根据技术维度对其进行横向切分,将限界上下文的领域层独立出来。
模块的划分不是这样,业务模块和基础功能模块泾渭分明。业务模块自身不具备支持业务能力的功能,如访问数据库、网络通信或消息队列,于是引入了业务模块与其他基础功能模块之间的依赖。在限界上下文中,这样的依赖(领域与基础设施之间的依赖)虽然依旧存在,但由于系统的划分边界是整个限界上下文,依赖发生在限界上下文内部,从架构层次看,相当于消除了依赖,变相地减少了依赖。
聚合
聚合在模型粒度与依赖之间引入了平衡。遵循对象建模的思维,建议为每一个领域概念都定义一个领域模型类,哪怕是email、quantity、address这样细小的基础概念,也当如此。Martin Fowler在重构中也提到,编码时要注意规避“基本类型偏执”坏味道。不用基本类型说明这些细小的领域概念,自然就需要定义对应的模型类了。
一旦为非常细小的领域概念定义了领域模型类,粒度就变得非常小,整个领域模型的类数量就会增加。增加了类的数量,必然也就会增加依赖。这时,聚合引入的边界就起到了很好的控制作用,它要求:
- 聚合内的领域模型由实体和值对象组成,形成一棵树
- 一棵树只有一个根,只有实体才能作为根
- 根实体作为聚合的唯一出口和唯一入口
- 跨聚合之间只能通过根实体建立关系
通过边界的控制,保障根实体之间才能建立关联关系,就去掉了许多非根元素跨聚合之间的关系,减少了领域模型类之间的依赖。根实体体现了对内部领域模型的封装,由它代表整个聚合内的所有领域模型,站在聚合边界之外,就可以认为领域模型类的数量减少了,调用者也无需关心聚合内部的其他实体和值对象。
限界上下文通过知识语境和业务能力形成的边界控制,减少了战略层面(也就是架构层面)软件元素之间的依赖关系;聚合则通过规定的设计原则与设计约束形成的边界控制,减少了战术层面领域模型类之间的依赖关系。
正因为如此,我才在《解构领域驱动设计》书中指出:
- 限界上下文是架构映射阶段基本的架构单元
- 聚合是领域建模阶段基本的设计单元
要做好领域驱动设计,在架构层面,限界上下文是不可或缺的,在设计层面,聚合才是不可或缺的。
降低依赖强度
当依赖不可避免时,需要将强依赖降低为弱依赖,也即所谓的“降低耦合度”。限界上下文作为基本的架构单元,要降低依赖强度,实则就是合理地管理限界上下文之间的协作关系,这是领域驱动设计的上下文映射模式所要处理的。防腐层(ACL)与开放主机服务(OHS)都降低了下游对上游的依赖,而发布语言(PL)则作为开放主机模式的补充,引入了对领域模型的封装。
上下文映射模式降低了限界上下文之间的耦合,强调了对内部领域模型的封装;对于限界上下文内部,则通过分层架构,凸显了领域模型的核心地位,利用层次(Layer)来分离关注点,并适当引入封装和抽象,解除了外部资源对领域模型,以及领域模型对外部资源的依赖。可概括为:
- 封装:引入应用服务,隐藏领域模型,包括领域模型中的聚合与领域服务,并保障应用层的轻和薄,严防死守,避免将领域知识泄露出去
- 抽象:引入资源库的接口,隔离对数据库的访问,且将资源库接口放到领域层,然后通过依赖注入实现依赖关系的反转
在《解构领域驱动设计》书中,我通过引入菱形对称架构将上述上下文映射模式、分层架构模式,以及应用服务与抽象资源库等内容全部囊括其中。开放主机服务属于北向网关,其中也涵盖了应用服务;防腐层属于南向网关,其中也涵盖了资源库,同时扩大了防腐层的外延,将所有对外部资源的访问都视为南向网关。至于发布语言,则介于外部网关层与内部领域层之间,就其本质而言,仍然属于外部网关层的一部分。
自治性
减少依赖数量,降低依赖强度,一言以蔽之,其实就是我们耳熟能详的六字法则“高内聚低耦合”。不管是架构原则还是设计原则,都是知易行难,知道“高内聚低耦合”的原则,并不能确保你做出符合该原则的设计。领域驱动设计通过限界上下文与聚合的核心模式,提供了相对可行的方法。若要解密领域驱动设计,此二者应为解密的钥匙。
为了更好地解密二者,我总结了它们共同的特性,将其名为“自治性”。要设计好限界上下文与聚合,就需要确保它们的自治性。
自治的限界上下文
自治的限界上下文需要具备如下四个特征:
- 最小完备:强调了每个限界上下文拥有的领域知识是最小完备的,它体现了限界上下文是领域模型的知识语境这一特征。
- 自我履行:强调了限界上下文在拥有了合理领域知识后,能够拥有一定智能,聪明地应对外界的请求,判断哪些该自己做,哪些该求助于别的限界上下文;自我履行的范围不仅限于领域知识,故而它体现了限界上下文是业务能力的纵向切分这一特征。
- 稳定空间:就限界上下文内部的领域模型而言,我们希望它是稳定的;在不考虑业务需求自身变化的情况下,要确保它的稳定性,就是要尽可能隔离它对外界的引用,将外界变化产生的影响降到最低;这就需要引入“抽象”,隔离外界,也就是菱形对称架构南向网关要做的事情。
- 独立进化:只要业务需求发生了变化,限界上下文内部的领域模型必然发生调整,但我们希望这种调整对于外界而言,可以变得静悄悄,虽然版本发生了进化,外界却可以无感知;其关键在于引入“封装”,不要将领域模型随便暴露在外,这实际上就是菱形对称架构北向网关要做的事情。
最小完备和自我履行结合起来,可以合理地减少依赖数量;稳定空间与独立进化配合起来,可以有效地降低依赖强度。显然,限界上下文的自治性满足了“高内聚低耦合”的架构原则。
自治的聚合
自治的聚合需要具备如下特征:
- 完整性:一个设计合理的聚合应该体现为最小单元的完整领域概念
- 独立性:每个自治的聚合都是相对独立的,它的生命周期也是独立的
- 不变量:聚合内部要维持各个对象之间关系的不变量,从而满足业务规则的约束
- 一致性:保持聚合内部各个对象的数据一致性,以及生命周期的一致性
完整性、不变量与一致性体现了聚合之“合”的一面,它意味着合入到聚合边界的模型对象是“高内聚”的;独立性体现了聚合之“分”的一面,意味着不要将生命周期不一致、数据不一致的模型对象放在同一个聚合里,聚合边界具有一种排斥能力,要将“异质体”推出去,通过聚合根实体来维持彼此之间的关系,保证聚合之间是“低耦合”的。
说明:限界上下文与聚合的自治性原则继承和延续了诸多促进优良设计的软件原则,是我个人对这些设计原则的总结和提炼,虽不能说是我的全新创造,但就其表现形式与内容而言,确实由我最早提出,特此声明。