DDD 起源
Eric Evans 的书:Domain Driven Design:Tackling the Complexity in the Heart of Software。
提出:通过领域模型,捕获领域知识,使用领域模型,构造更易维护的软件 。
软件开发的核心难度在于:处理隐藏在业务知识中的复杂度。模型,就是对这种复杂度的简化和精炼。处理这种复杂度,需要打破业务和技术双方的知识壁垒(统一语言),从而使知识传递、需求沟通变得更高效。
为什么需要领域模型
模型驱动的思路。程序 = 数据结构 + 算法。 通过数据结构出发构造模型,通过算法来解决问题。
在软件开发的早期,树、图、链表、堆、栈等与领域无关的模型,帮助我们解决了编译器、内存管理、数据库索引等大量问题。无数的成功案例给了我们一种习惯:将问题转化为与领域无关的数据结构,即构造与领域无关的模型。
而如果做的是业务软件,就会引入不具备开发背景的业务方参与进来。这个时候,与领域无关的数据结构及其关联的算法,业务方并不理解,也无法直观地映射为业务上的流程和功能。这种认知的差异,就会造成团队的沟通困难。
显而易见的,解决办法就是,构造与领域相关的模型,将业务上的流程和功能,转化为模型的行为。
怎么构造领域模型
Eric、8x提出及总结:不断循环往复“知识消化(Knowledge Crunching)”的步骤,迭代改进的试错法。
crunching,指的是吃薯片时发出的那种难以忽略的咔嚓声。那 knowledge crunching,就意味着我们捕获领域知识的时候,要大声的、引人注意的去获得反馈,哪怕这个反馈是负面的。
知识消化:两关联一循环。
- 模型和软件实现关联
- 模型和统一语言关联
- 提炼知识的循环
模型和软件实现关联
Eric 在知识消化中,并没有强调模型好坏的重要性,反而非常强调模型和软件实现间的关联。
模型不求一步到位,但求一次比一次好,技术方和业务方以迭代反馈的方式,通过提炼知识的循环,逐步完成对模型的淬炼。比起模型的好坏(总是会改好的),模型和软件实现关联就显得更为重要了。
如果模型和软件实现不关联的话,就会导致分裂出 2 个模型:更接近业务方使用的分析模型,更解决软件实现的设计的模型。此时,分析模型就退化成了纯粹的需求沟通的工具,脱离了实现细节,分析就会天马行空、不着边际。
将模型和软件实现关联,一种做法是开发“充血模型、富含知识的模型(Knowledge Rich Model)”。也就是强调了面向对象技术在表达领域模型上的优势。
贫血模型(Anemic Model) --> 充血模型(Knowledge Rich Model) :
贫血模型:对象仅仅是简单的对数据的封装,关联关系和业务计算都散落在对象的范围之外。
充血模型:对象里,包含了某个概念的行为与逻辑。
比如,极客时间中,用户和他订阅的专栏。可能会有2个逻辑:获取用户所有订阅专栏、计算用户所有订阅专栏的总价。
贫血模型的写法:
record User() {}
record Subscription() {}
class UserDAO {
public User find(long id) {
// do something ...
return null;
}
}
class SubscriptionDAO {
public List<Subscription> findSubscriptionByUserId(long userId) {
// do something...
return null;
}
public double calculateTotalSubscriptionFee(long userId) {
// do something...
return 0.0;
}
}
充血模型的写法:
/**
* User 是聚合根 Aggregation Root。
* Subscription 无法独立于 User 而存在。
*/
class UserModel {
public List<Subscription> getSubscriptions() {
// do something..
return null;
}
public double getTotalSubscriptionFee() {
// do something...
return 0.0;
}
}
class UserRepository {
public User findById(long id) {
// do something...
return null;
}
}
通过聚合关系链接在一起的对象,从概念上他们就是一个整体。在这个例子中,充血模型无法构造一个独立于 User 存在的 Subcription 对象。就这把 Subcription 相关的业务功能,映射到了 User 这个模型中,也就是模型和软件实现关联起来了。
模型和统一语言关联
统一语言(ubiquitous language):特指根据领域模型构造的一种共同语言,由业务方和技术方共同使用。
这里的业务方,泛指一切非最终软件实现者,比如:客户、产品、业务、解决方案架构师、用户体验设计师等。
这里的共同语言,也有很多种表现形式,比如:用户旅程(User Journey)、用户画像(User Persona)、数据字典(Data Dictionary)等。
统一语言的形式并不重要,或者说统一语言可以有任意一种形式。
为什么要有统一语言
在理想情况下,业务方和技术方可以直接通过模型来进行交流。但在实际操作的过程中并不理想。
业务维度(Business Perspective)被隐藏了。
模型更偏重于数据角度,描述了在不同的业务维度下,数据会如何改变,以及如何支撑对应的行为。而业务方则更多的是从流程、交互、功能、规则、价值等去描述软件系统。
此外,模型是从已知的需求中总结提炼的知识,就是说,单纯使用模型,一定会有无法表述的需求。
更确切的说,我们需要一种能与模型相关的共同语言,他能够让模型在核心位置扮演关键角色,也能弥合技术业务视角的差异,并提供足够的缓冲。
相对于模型的精准,统一语言的模糊,更能满足人与人之间交流的需求。
统一语言的好处
修改代码就会修改模型,修改模型就会修改统一语言。
代码的修改,不一定是需求的变更,也可能是代码或重构。通常情况,技术方通过不断修改代码,可能会影响到业务模型,但这种模型的变化很难传递到业务方,从而引起非预期的问题,可能就会导致双方分歧越来越大。
引入统一语言,就可以将这种变化传递给业务方,在用统一语言描述需求时,就有机会验证这种改变是否在正确的方向上。
示例
统一语言可以包括:
- 源自领域模型的概念和逻辑
代码实现中的实体或行为方法。
- 界限上下文(Bounded Context)
围绕某些模型设置的边界,对于如何用边界内的模型有清晰的想法,这个想法由边界保持一致。
可能和子域(subdomain)等表示的范围重合,也可能是自己界定的稀奇古怪的范围。
- 系统隐喻
比如:要做某某领域的淘宝、要做某某领域的滴滴。表示了产品的愿景。
- 职责的分层
关注稳定性。
- 模式与惯用法
业务规则、流程、实现模式
比如,上面例子中的 用户订阅专栏的情况。
可以提取出源自模型的概念,也就是领域模型中的实体:
- 用户,User:指所有在极客时间中注册过的人
- 订阅,Subscription:指用户付费过的专栏
由于 User 和 Subscription 之间的关系,还可以提取出一个业务逻辑:
- 用户可以订阅多个专栏
也可以提取出一个界限上下文:
- 订阅
提炼知识的循环
是什么
是以模型为最终产物的研发流程。
提炼知识循环的流程
- 通过统一语言描述、讨论需求
- 发现模型中缺失或不恰当的概念,从而凝练知识、修改模型
- 以试验或头脑风暴,验证模型、统一语言的准确性
如此往复循环,不断完善模型和统一语言。
可以看作通过统一语言对模型的重构:
- 发现坏味道 – 通过讨论需求发现模型中缺失/不恰当的概念
- 尝试消除坏味道 – 修改模型,凝练知识到模型中
- 判断坏味道是否消除 – 试验或头脑风暴,验证新的模型是否准确
业务方是发现坏味道的人。
也可以看作通过统一语言对模型的测试驱动开发:
- 构造失败的测试用例,表明需求的变化 – 通过讨论需求发现模型中缺失/不恰当的概念
- 修改代码实现 – 修改模型,凝练知识到模型中
- 通过测试,证明需求以完成 – 试验或头脑风暴,验证新的模型是否准确
业务方是构造测试的人。
示例
比如,基于上面例子中抽取出的统一语言中,有这样 2 个需求:
作为一个用户 User,当我查询购买过的专栏 Subscription 时,可以看到其中的教学内容。
当用户 User 已购买过了某个专栏 Subscription,那么当他访问这个专栏时,就不需要再为内容付费。
在我们的统一语言中,模型中无法表达这样的需求,因为只存在用户付费过的专栏 Subscription:
作为一个用户 User,当我对某个专栏的内容感兴趣时,我可以购买这个专栏,使其成为我付费过的专栏 Subscription。
然后我们头脑风暴,提炼知识,修改模型,可以提取出如下的统一语言:
业务概念:
- 用户,User:指所有在极客时间中注册过的人
- 订阅的专栏,Subscription:指用户付费过的专栏
- 专栏,Column:是一组付费内容 Content 的集合,由极客时间签约的作者 Author 提供
- 付费内容 Content:是课程的载体,可以是文字、视频、音频
- 作者 Author:是极客时间寻找的在某些领域有经验与专长的实践者
业务逻辑:
- 用户可以订阅多个专栏
- 专栏中可以包含多个付费内容
- 统一作者可以发布多个专栏
界限上下文:
- 订阅
- 课程发布
之前无法描述的需求,就可以描述了:
作为一个用户 User,当我对某个专栏 Column 的内容 Content 感兴趣时,我可以购买这个专栏,使其成为我付费过的专栏 Subscription。
领域驱动设计是什么
一种协同工作方式
对于技术方来说,有权利:修改代码后,会影响模型/统一语言,从而可以定义业务。同时也要承担义务:在知识消化的循环中接受业务对模型->代码的影响。
对于业务方来说,有权利:在知识消化的循环中,会影响模型,从而改变软件实现。同时也要承担义务:接受技术通过模型/统一语言来定义业务。
这种权责分明让双方都参与到了对方的工作当中,是一种没有绝对主导地位的合作关系,从而可能产生 1+1>2 的协同效应(Synergy Effect)。
但这种协同方式能否起作用,很大程度上依赖于团队对模型、统一语言的理解和接纳。部分程度上依赖于建模者的变革管理能力。
一种迭代试错的建模方法
领域驱动设计名字里虽然有设计,但很少谈及具体的模型设计,反而更偏向通过交互来协同试错。
领域驱动设计要解决的问题域是复杂问题,是没有现成答案的问题,那迭代试错就是唯一“可行 Viable”的方式了。
所以,无法保证迭代之后的模型是好的、成功的,这是所有试错法都存在的问题。
Eric 的观点:知识消化是一个探索的过程,你不可能知道你会在那里停止。当你停止时,你可能还不知道得到的是垃圾还是宝藏。
模型是对问题的抽象,没有对错,只有角度不同。
比如,前面知识消化中缺失的专栏、内容模型,可以把 订阅 Subscription 看作是 专栏的一个特殊状态:
或者也可以把 Subscription 看作是一种关联对象:
哪种模型更好呢?不知道,要看在具体需求上哪一种能更好的应对变化。
参考资料:
极客时间中徐昊老师的专栏:《如何落地业务建模》