本文对《领域驱动设计-软件复杂性应对之道》一书进行高度凝练,梳理了领域驱动设计的架构图、基本要素和重要概念。从细节入手,以小见大,你想知道的定义,这里都有。
用过DDD的同学会知道,自己用倒没什么,一旦到了要推广部门内使用的时候就会非常困难。为什么会这样?做为一个有8年DDD实战经验的过来人,我想尝试分析一下原因:
行吧,那我们姑且撇开那些枯燥的概念,只一篇文章也聊不明白(当然我也得假设你至少听说过这些概念,否则你不会点进来看这篇文章)。接下来我们就只探讨DDD的思想内核,或许能稍稍降低一些理解门槛,尽量让大家明白“哦,原来DDD是干这个的!”
一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。如果该成本很低,并且在系统的整个生命周期内一直都能维持这样的低成本,那么这个系统的设计就是优良的。如果该系统的每次发布都会提升下一次变更的成本,那么这个设计就是不好的。——架构整洁之道 |
---|
就像《架构整洁之道》一书所说,如果一个系统的每次发布都会提升下一次变更的成本,那么这个设计就是不好的。那么DDD战术设计推崇的架构模式是如何控制变更成本的呢?
我们知道,架构与底层工具应该是完全独立的,一个良好的架构应该围绕业务来展开,这样的架构设计可以在脱离架构、工具及使用环境的情况下完整的描述业务用例。并且,架构应该在满足业务需要的前提下,尽可能地允许用户能自由的选择工具。
此外,良好的架构设计应该尽可能地允许用户推迟和延后决定使用什么框架、数据库、web服务以及其它与环境相关的工具(例如spring、hibernate、mybatis、数据存储、消息组件等)。同时,良好的架构还应该让我们能很容易的改变这些决定。总之,良好的架构应该只关注业务用例本身,并能将它们与其它周边因素隔离开来
在最初设计时,多数分层架构为了层级职责清晰,会比较偏向严格分层架构。但是经过多次的转手和迭代后,严格分层架构会逐步演变成松散分层架构,各层职责越来越不清晰。比如可能为了方便,直接在controller里直接通过dao访问数据库;service的职责也越来越混乱,里面可能混杂了基础设施的远程调用、资源库获取等等不属于它的职责。长此以往,对系统的可维护性带来巨大的挑战,成员之间互相接手时需要花费大量的时间才能真正上手,随时都会有踩雷的风险,无法做到研发资源的最大化利用。
那么DDD的内核是什么呢?
先来看一张架构图示例:
这是现在很典型的一种架构分层,横向先根据不同技术领域先分几层,大差不差也不会有什么人挑战。问题出在应用层和微服务层,实际上很少人能说清这两层分类的标准是什么?高内聚低耦合吗?就是这句看起来说了但又什么都没说的话在指导着架构设计。有没有更靠谱一点的方法论?这就是DDD想说的第一件事,它提供了一种给“应用层”或“服务层”分类的方法。
接下来DDD想告诉你第二件事:保护好你的代码边界,否则它会变得腐化且难以维护。这句看起来理所当然的话事实上并没有那么容易做到,首先“边界”并不那么好界定,其次为了守住边界就得立一些规范,有时候为了赶项目进度,边界是可以模糊的,要知道一旦模糊了以后再想规范起来可就没那么容易了。
保护好边界后,面对边界内的核心代码,接下来就是DDD想说的第三件事:让模型回归业务的本质。
以上就是DDD内核的全部内容,接下来我想调换一下顺序,先聊一聊看起来最抽象的第三件事。
正式开始之前,我想先聊一聊问题空间和解空间。
问一个问题,把小球向斜上方30°以3m/s的速度扔出,小球多久能落地?小球落地时扔了多远?这是一道初中物理题,为了解这个问题,我们需要进行建模,忽略空气阻力的情况下小球会呈抛物线运动,结合三角函数和动力学方程,很容易能得出问题的解。(没算错的话大约是0.2s,扔的距离大约是0.4m)
可以看到问题空间到解空间的过程需要经过一些抽象,”抛物线“就是一个抽象,我们还要忽略空气阻力,暂不考虑扔球者的身高,合理的建模就能得到问题的解。然而解空间永远不可能等同于问题空间,只可能无限接近。比如你无法模拟出小球运行过程中的空气阻力,也不会为了算一道题就去测量常量g的数值,一般取9.8m/s²。
如果解空间的建模与问题空间相差较大(上右图),也就是说引入了问题空间以外的因素,例如抛球时人手心有没有冒汗,当天的空气湿度之类的。也许问题依然可解,但那会凭空增加许多复杂度。
在软件领域,问题空间就是业务规则,解空间就是你所建立的系统,你通过建立系统把这个世界的一部分规则通过数字化的方式运行起来。我们理想的方式是解空间是问题空间的一个子集,尽量不引入问题空间以外的因素,这就是让模型回归业务本质的含义。
怎样才能回归本质呢?DDD说要对领域进行建模,所谓的领域(Domain),大白话就是业务规则和业务概念,领域模型就是业务规则的集合。完整的领域模型应该是要包含服务(Service)、事件(Event)和实体(Entity)、DP(约等于ValueObject)等
建模的过程大家应该不陌生,就是用UML工具把上面这些东西画出来,现在问题在于怎样让模型更接近业务的本质?DDD提供了一些方法和辅助工具:
这些方法有些听起来挺抽象,但只要回归到原点:让模型尽量还原业务,你就不会再纠结于这些概念。我们目标是建立一个能让人更易于理解的系统,我觉得评判标准也很简单,如果建立的模型在无需额外解释的情况下,技术能看懂、产品能看懂、业务专家也能看懂且各方达成一致,这个模型就是成功的。
上面说到业务模型要回归到业务本质,那么非业务的部分怎么处理?我们总要做分库分表、总要引入中间件,以及为了性能考虑经常需要做字段冗余。
这就是DDD强调的边界之一:技术代码和领域模型的边界。
这一边界的重要性不言而喻,我们一定不会希望有一天因技术架构的升级导致业务模型大改,在理想的情况下,业务模型和技术架构的演进是正交的。什么是正交?就像直角坐标系的x轴和y轴那样,分别朝不同的方向延伸,互不影响。
要做到这一点,我们就要合理地使用依赖倒置(Dependence Inversion),把技术细节封装在基础设施层(Infrastructure Layer)。例如数据存储就离不开对数据库的依赖,这时候如果让领域层直接依赖数据库层就会让数据库的技术特性(分库分表、事务、索引等)影响领域模型,这就导致了边界被打破,引入repository层就能解决这个问题:
我们的系统一定是由许多子系统或服务组成,子系统之间会有联系和依赖,有依赖就会有边界,这就是DDD说的边界之二:业务上下游的边界。
DDD为了更好地聊边界,直接引入一个新名词叫限界上下文(BC:Bounded Context),实在理解不了就把BC当成一个系统或服务就好了,BC之间的关系叫上下文映射(Context Mapping),上下文映射有很多种,篇幅原因就讲最常见的两种。
第一种:为了能更好地了解BC之间的协作关系,首先得区分上游(upstream)和下游(downstream),就像水流一样,上游的水脏了就会影响下游的水,怎样能降低影响呢?在中间建一个过滤网不就好了,这个过滤网就是防腐层(ACL:Anticorruption Layer)
注意区分上下游关系,受伤的总是下游,因此ACL也建在下游。通过ACL把上游传递过来的数据转化为下游可理解的模型,这样可以降低上游变化对下游核心模型产生直接的影响。
第二种比较常见的模式,两个BC之间有共享的部分,叫做共享内核(Shared Kernel)。这种情况下可以把共享内核抽出来单独做一个小型的BC,为啥要小型呢?因为这种模式存在一定风险,共享内核的修改会影响多个下游。
现在来想一想,你是否守住了自己的代码边界?可以评估一下,如果说下面这些事项都不需要侵入或改动你的核心模型就能实现,那就可以说代码边界是比较稳固的。
现实里在程序员之间经常听到这样的争论:
实际上这都是在说一件事,就是如何做系统和功能的划分。我认为架构设计到最后就是在做分类:怎么把内聚的功能划分在一起,同时又要考虑把风险隔离开。如果你要问一个架构师怎么做架构设计,也许他会嘿嘿一笑:“高内聚低耦合”。确实架构设计某种程度上是比较主观的,但也并不完全没有方法论可循,DDD在这方面就提供了一些参考。
- 从参与者出发思考
做过用例(Use Case)分析的都知道,参与者(Actor)是与系统交互的主体,参与者很大程度上决定了用例设计的方向,例如同样都是“查询交易记录”,从普通用户角度和从客服运营的角度出发去设计可能会差异巨大。因此按照参与者的不同来拆分可能会是个不错的选择,这样可以避免不同参与者的“利益”相互影响。
- 从模型角度去思考
我们在设计模型的时候,“类图”尤其好用,类图的元素比较全,既包含服务又包含事件和实体(如果你画的是完整类图的话)。从图上很容易能识别模型的请疏远近,这时候高内聚低耦合已经被具象化了,再去划分会变得so easy!
(随便画个示意图,大家能理解意思就行~)
- 从风险隔离角度去思考
在业务规模不大的情况下,或许这个因素会显得没那么重要。一旦业务规模上升到一定量级,“风险隔离”在架构设计中会越发占据更多的比重。
举一个电商业务的例子,在电商平台买东西的流程中,支付、扣减库存、生成订单这些环节应该都属于促成交易的“主链路”上,而生成物流单、支付成功后加积分这些环节就不在主链路,特别是在双11这样的高并发场景下,你一定不愿意看到因用户加积分这个功能挂掉导致用户整个交易失败。
因此在必要的时候,给每个功能标记一下风险级别,这也是一种分类的办法。
- 从组织架构思考
“康威定律”在IT界一定不陌生,这个定律揭示着组织架构与系统架构之间存在着微妙联系。我对这一条定律有深刻的体会,因为我曾在这样两种截然不同的组织架构下工作过
a. 在一级部门下先划分业务部门、产品部门、技术部门,技术部门下面再划分不通的业务方向。这种组织架构下技术部门有较高自治权,容易孵化出各种中台,技术选型和技术路线决策也会更加统一,人们会倾向于去思考“全局最优解”,但在实施业务战略的时候,需要更加费点劲才能形成组织合力
b. 先划分“事业线”,也就是业务方向,在事业线内部再划分业务部门、产品部门、技术部门。这种组织架构下每个业务各自为战,优先保证的是自己活下来,机动性强,但缺少全局统筹,技术架构通常是割裂的,一直在重复造轮子
不同的组织架构将会直接影响这个组织的思考和运作方式,在做架构设计的时候,技术架构不要试图去和组织架构做对抗,而是要协同。
我在阐述这一节的时候尽量避免用DDD本身的术语,毕竟我是希望透过现象看本质。但如果你看到这里已经对DDD产生了兴趣,不妨可以再查阅资料深入研究一下限界上下文(BC),上面所说的这些实际上都是在说BC,可以毫不夸张地说,BC是整个DDD最核心的一个概念(同时也最不好理解- -||)
现在再来看这张DDD的典型架构图,你可能会更加理解DDD的思想内核,这是一种面向领域的设计方法,把领域层(业务规则)包在最里面,保护好边界,避免领域层被污染。DDD通过这样的方式降低建模和实现的复杂度。
一、构造块
构造块是指领域设计中最基本的一些元素。
构造块大图
1.1 分层架构
应用层和领域层的关系
分层是常用的方法,不仅是领域驱动设计,计算机网络也会看到分层思想:网络的七层模型。分层的好处在于:可以集中精力关注每一层的功能职责,更好地分工协作。
分层的基本原则是:层中的任何元素都仅依赖于本层的其他元素或其下层的元素。下层如果不得已要调用上层的话,需要采用合适的模式,如观察者模式或者回调机制等。
常见分层如下:
cn.huolala.demo.domain.model
包中定义了当前限界上下文所需要的所有领域模型(包括了非当前上下文),这些模型在DDD中也称为实体,它们都是充血模型,业务的聚合都由它们(cn.huolala.demo.domain.model.order.Order
) + 领域服务(如cn.huolala.demo.domain.service.OrderDomainService
)来实现。(包cn.huolala.demo.domain.infra
),以此来实现基础设施的依赖倒置。这里的做法是把基础设施做了进一步的拆分(例如资源库repository、远程服务调用remote、消息事件mq);还有一种比较好的实践:不对基础设施做进一步的区分,把所有的基础设施都视为资源,只定义资源库接口,这样的好处是在修改技术组件时,业务逻辑层可以做到0改动。注:还有另外一种分包方式,就是把跟领域模型相关的所有基础设施放到一个包中,好处是可以使业务模型更加稳定,弊端是会破坏组件的封装性。 |
---|
DDD架构模型的分包规则符合最大复用和最小闭包原则,各层模型分包独立,互不影响。
例如:作为用户界面层,controller和provider返回的模型可能会是不一致,所以我们把具体的返回模型包装在在各自的层级中,应用服务层只负责输出领域模型,由各个用户界面层决定把领域模型转换为各自返回给客户的模型。
1.2 关联
模型其实是真实世界模型的一个子集,不需要每个细节都反映出来,关联关系也是如此。
关联描述的是对象之间的联系,作者描述了3种减少关系复杂度的方法:
依赖关系示意
1.3 entity
实体 entity 的定义:主要由标识定义的对象被称做entity,特点是在整个生命中具有连续性。
简单理解为领域层中带有意义ID的对象,如:订单信息(含订单ID)。
1.4 value object
值对象 value object 的定义:用于描述领域的某个方面而本身没有概念标识的对象称为 value object。
简单理解为对象属性决定差异的个体,如:颜色RGB对象,只要rgb值一致,那么其实是一种颜色,并不关心对象的本身到底是哪个实例。
这类对象特点:
1.5 service
领域层中服务 service 的特征:
简单理解为领域层中一些需要协调多个对象的无状态函数。可以暴露领域中的一些功能。
值得注意的是:
1.6 aggregate
聚合 aggregate 是一组相关对象的组合,有一个根(root)和一个边界(boundary)。外部对象只能引用根,而边界之间的内部对象之间则可以相互引用。
聚合根示意图
1.7 factory
工厂 factory 负责复杂对象和aggreate的创建,是领域设计的一部分。
1.8 repository
存储库 repository 为那些需要直接访问的对象提供数据访问能力,封装底层存储逻辑。
1.9 specifiction
声明 specifiction 是为特殊目的创建了谓词形式的 VALUE OBJECT,是一个谓词,可以用来确定对象是否满足某些标准。
场景:某些业务规则不适合作为Entity 或 VALUE OBJECT 的职责。
通过图书馆管理系统的比喻,可以更直观地理解声明的作用:
class AgeSpecification {
private int minAge;
public AgeSpecification(int minAge) {
this.minAge = minAge;
}
public boolean isSatisfiedBy(Reader reader) {
return reader.getAge() >= minAge;
}
}
java
复制
Reader reader = new Reader("Alice", 14); // 读者 Alice,年龄 14
AgeSpecification spec = new AgeSpecification(12);
if (spec.isSatisfiedBy(reader)) {
System.out.println("读者符合借阅条件");
} else {
System.out.println("读者不符合借阅条件");
二、柔性设计
柔性设计(supple design):运用深层模型所蕴含的潜力来开发出清晰、灵活且健壮的实现,并得到预期的效果。
柔性设计关键要素
2.1 intention-revealing interfaces
释意接口 intention-revealing interfaces:在命名类和操作时要描述它们的效果和目的。
2.2 side-effect-free function
无副作用函数 side-effect-free function:尽可能把程序的逻辑放到函数中,函数不产生明显的副作用,把命令(引起明显状态改变的方法)隔离出来。
2.3 assertion
断言 assertion:对程序某个时刻正确状态的声明,在编程中编写 assertion 或者单元测试。
2.4 conceptual contour
概念轮廓 conceptual contour:领域本身的一致性,把设计元素分解为内聚的单元。
2.5 standalone class
孤立的类 standalone class:无需引用任何其他对象(系统的基本类和基础库除外)就能够理解和测试的类。把无关对象提取到对象之外,保持低耦合。
2.6 closure of operation
闭合操作 closure of operation:闭合操作提供了一个高层接口,同时又不会引入对其他概念的任何依赖。在适当的情况下,可以定义操作的返回类型与参数类型相同。
三、战略设计
战略设计(strategic design):一种针对系统整体的建设和设计决策。
战略类型
3.1 bounded context
限界上下文 bounded context:特定模型的界限应用。限界上下文使团队知道什么必须保持一致,什么必须独立开发。
3.2 continuous integration
持续集成 continuous integration:把工作足够频繁地合并到一起,并使它们保持一致。
3.3 context map
上下文图 context map :项目所涉及的界限上下文以及它们与模型之间的关系的一种表示。描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。
上下文映射示意
3.4 shared kernel
共享内核 shared kernel :通常是共享核心领域或者是一组通用子领域。
共享内核
3.5 customer/supplier
客户/供应商关系 customer/supplier:上下游关系。不同客户需要协商来平衡,上游团队需要有自动测试套件。
客户/供应商关系
3.6 conformist
跟随者模式 conformist:单方面跟随模式。上游的设计质量较好,容易兼容,可以采用严格遵循上游团队的模型。
跟随者模式
3.7 anticorruption layer
防腐层 anticorruption layer:防腐层、隔离层,使用 facade or adapter 等模式。可以减少其它系统变动对本系统的影响。
防腐层
3.8 separate way
各行其道 separate way:声明一个与其它上下文毫无关联的 bounded context,使开发人员能够在这个小范围内找到简单、专用的解决方案。
各行其道
3.9 open host service
开放主机服务 open host service:开放子系统供其他系统访问。
开放主机服务
3.10 published language
共享语言 published language:把一个良好文档化、能够表达领域信息的共享语言作为公共的通信媒介,必要时在其它信息与该语言之间进行转换。
四、精炼
精炼(distillation):是把一堆混杂在一起的组件分开的过程,从中提取出最重要的内容,使得它更有价值。
精炼要素
4.1 core domain
核心领域 core domain:模型中最关键的部分,是程序的标志性部分,也是应用程序的核心诉求。
4.2 generic subdomain
通用子领域 generic subdomain:识别出对项目意图无关的内聚子领域,提取通用模型,并分离出来。
4.3 domain vision statement
领域愿景说明 domain vision statement:类似愿景说明文档,关注领域模型的本质,以及如何为企业带来价值。
4.4 highlight core
突出核心 highlight core:编写简洁文档描述 core domain 以及核心元素之间的主要交互过程,并把 core domain 标记出来。
4.5 cohesive mechanism
内聚机制 cohesive mechanism:把内聚的部分分离到一个单独的轻量级框架汇中, 并用释意接口暴露框架功能,从而使得领域元素关注“做什么”,“怎么做”转移给了框架。
4.6 segregated core
分离的核心 segregated core:把核心概念从支持性元素中分离出来,并增强核心的内聚性。把通用元素、支持性元素提取到其它对象中。
4.7 abstract core
抽象核心 abstract core:识别模型中最基本的概念(能表达出主要组件的大部分交互),并分离到抽象模型中(类、抽象类或接口)。抽象模型放在自己的模块中,实现类留在子领域定义的模块中。可以减少模块间依赖的复杂性。
抽象核心
五、大型结构
大型结构(large-scale structure):一组高层的概念、规则,它为整个系统建立了一种设计模式,能够从更大的角度来讨论和理解系统。
大型结构要素
5.1 evolving order
演变有序 evolving order:让概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同的结构风格。
演化示意
5.2 system metaphor
系统隐喻 system metaphor:当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向着一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。例如:用真实大楼的防火墙来类比网络的防火墙。
防火墙类比
5.3 responsibility layer
职责分层 responsibility layer:在具有自然层次的模型中,可以围绕主要职责进行概念上的分层,这样可以结合使用”分层“和”职责驱动的设计“这两个有力的原则。
地球的“分层”
5.4 knowledge level
知识级别 knowledge level:是一组描述了另一组对象应该有哪些行为的对象。当我们需要让用户对模型的一部分有所控制,而模型又必须满足更大的一组规则情况时,可以利用这个模式来处理。可以通过类型的知识,去选择不同的策略。
知识级别
5.5 pluggable component framework
可插入式组件框架 pluggable component framework:从接口和交互中提炼出一个抽象核心 abstract core,并创建一个框架,这个框架要允许这些接口的各种不同实现被自由替换
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。