序言
领域驱动设计(Domain-Driven Design,DDD)提出距今已经有 20 年的历史,虽然前十多年的时间都一直处于不温不火的状态,但一直在顽强的生长。最近几年,微服务大行其道,DDD 也开始普及了起来。我们完全可以断定,是微服务的热风让研发人员重新发现了 DDD 的价值,再加上敏捷软件开发和 DevOps 的发展为 DDD 的落地铺好了道路,DDD 终于咸鱼翻身,星火燎原。可以说,微服务就像是 DDD 的心上人,使得 DDD 真正焕发起了青春。
微服务架构从提出以来一直没有很好的理论支撑如何合理地划分服务边界,研发人员常常为服务要划分多大而争吵不休,而 DDD 被发现恰好可以弥补微服务的营养不良:
- 服务最大不要大过一个限界上下文(Bounded Context),否则服务内可能会存在有歧义的领域概念;
- 服务最小不要小过一个聚合(Aggregate),否则会引入分布式事务的复杂度;
- 服务间最好通过领域事件(Domain Event)来进行交互,这样可以让服务保持松耦合。
微服务与 DDD 的结合,让微服务架构看起来似乎更加稳健了。
近年来,学习 DDD 的研发人员越来越多,很多学员反馈 DDD 很神秘,不容易掌握。本文尝试揭开 DDD 的神秘面纱,包括下面七层:
- DDD 发展了 OO;
- DDD 发展了敏捷;
- 统一语言;
- 战略设计;
- 战术设计;
- 分层架构;
- 两关联一循环。
面纱一:DDD 发展了 OO
在标准的 UML(Unified Modeling Language)过程中,软件研发被明确的分为面向对象分析(OOA),面向对象设计(OOD)和面向对象编码(OOP)阶段:
- 在 OOA 阶段,分析师(领域专家或业务专家)以需求为输入,完全从业务视角出发,对需求中的领域概念进行分析和提炼,并定义它们之间的关系,建立分析模型;
- 在 OOD 阶段,设计师(架构师)以分析模型为输入,从技术视角出发,补充被分析师忽略但却与软件开发效率息息相关的因素(软硬件平台、数据存储约束、并发、编程框架...),进一步按照软件工程的要求(各种设计原则和模式)重塑了分析师给出的分析模型,建立设计模型;
- 在 OOP 阶段,程序员(开发人员)根据设计模型来编写代码,并根据代码的反馈来演进设计模型。
分析师是业务人员(业务方),而设计师和程序员都是技术人员(技术方)。OOA 方法虽然在理论上是完备的,但在实践中却常常会有问题。由于业务人员在 OOA 阶段缺少技术视角考虑,那么分析模型在转化为设计模型的时候,可能会很难实现,需要通过复杂的转换才能适应软件开发的要求,因此两种模型就有了比较大的 gap。
在之后的开发过程中,程序员往往只是聚焦于设计模型,而将分析模型束之高阁。一方从任何研发活动中获得的知识都无法流畅的传递给另一方,导致分析模型与设计模型之间的差异越来越大,分析模型变得天马行空不着边际,也就失去了存在的意义。
许多系统的真正复杂之处并不在于技术,而在于领域本身和业务用户及其执行的业务活动。如果在软件设计时没有获得对领域的深刻理解,没有通过模型将复杂的领域逻辑以模型概念和模型元素的形式清晰地表达出来,那么无论我们使用多么先进、多么流行的基础设施,都难以保证项目的真正成功。
DDD 抛弃了将分析模型与设计模型分离的做法,而是寻找单个模型来满足两方面的要求,这就是领域模型。领域模型是业务视角和技术视角的交集,它反映了业务人员和技术人员的共识。对领域模型的建立,必须由业务人员和技术人员达成一致。随着领域模型的演进及精炼,技术实现越来越逼近业务的本质,软件系统可以真实反映业务需求,且易于理解和维护。
面纱二:DDD 发展了敏捷
在敏捷软件开发中,团队更倾向于被定义为一个个独立的特性团队。在特性团队内部,业务专家、架构师、开发人员和测试人员要能够无障碍的沟通,并集体为特性的端到端交付负责。一种被普遍接受的观点是“软件源代码是唯一真正的设计产物”,一些偏重技术实践的敏捷软件开发方法(如 XP)极大的推动了这方面的实践:技术人员优先将精力投入到代码上,持续保持代码的清晰和灵活性,通过重构代码来演进设计模型,并通过自动化测试来确保设计演进的正确性和安全性。
技术人员通过代码不断修改实际的业务模型,但这种变化很难有效传递给业务人员,于是这种变化就成为了“隐秘的角落”。同时,也很难让业务人员以技术人员改变后的业务概念去思考问题。最终导致双方互看不爽,分歧越来越大。因此,看起来匪夷所思的情况,实际上一直都在发生。技术人员一直都在定义业务,只是没有合理的途径让业务人员了解并接纳而已。
领域模型从观念上改变了这一状况,它将大家从各自的领地,也就是业务与技术中,拉到了一个中间地带。领域模型既不完全属于业务,也不完全属于技术,而是双方共有的部分。于是技术人员与业务人员就有了差不多的话语权,至少有了可以沟通和协同的空间。
一旦业务人员接受了领域模型,实际上就是放弃了对业务 100% 的控制权,也意味着领域模型能够赋予技术人员定义业务的权利。但领域模型在给技术人员带来额外的权利同时,也隐含着额外的义务,即技术人员在领域模型的实现过程中,要接受业务人员的监督与反馈。如果业务人员不同意技术人员对领域模型的修改,那么技术人员需要尽快修复这种误差,就是说技术人员丧失了对代码 100% 的控制权,也意味着领域模型能够赋予业务人员影响软件实现的权利。让业务人员与技术人员参与到对方的工作中,可以打破双方的知识壁垒,知识传递与沟通的效率就变得更高,反馈速度就变得更快,同时浪费也变得更少,所以说这是一种更好的协同方式。
DDD 发展了敏捷,它显式地把领域和设计放到了软件研发的核心,业务人员和技术人员被得到同样的重视,他们通过协同来构建并演进领域模型,这让敏捷软件开发方法真正的在领域知识复杂的行业内得以有效应用。
面纱三:统一语言
一般情况下,业务人员嘴里全是业务,技术人员嘴里全是技术。业务人员对问题域的理解很深刻,但却不太关注解决方案域,而技术人员对解决方案域非常熟悉,但对问题域的理解却不充分。随着业务的发展和变化,双方之间就逐步形成了一道人为的鸿沟。
统一语言(Ubiquitous Language),也叫通用语言,是业务人员和技术人员共同创建的一套语言,必须在团队范围内达成一致。团队所有成员使用统一语言进行交流,每个人都能听懂别人在说什么。显然,创建统一语言就是定义团队成员的“普通话”,团队所有角色在脑海中对领域知识生成的画面是高度一致的,如下图所示:
统一语言,将技术人员的思考起点拉到了业务而不是技术上,也就是从问题出发,这就在一定程度上填平了那道人为的鸿沟。
业务人员和技术人员如何建立统一语言?最简单的做法就是让业务人员和开发人员一起,找一块白板,把各种概念都写在上面,然后整理出领域名词、领域动词和领域规则。这里面的重点是,让业务人员和开发人员在一起。如果只让一方出现,结果又会是原来的样子,因为你没法判断,这里面的语言对方是否听得懂。这种做法很简单,但通常都不够系统,会存在各种遗漏。2013 年,一位叫 Alberto 的 DDD 专家提出了一种更正式的实践——事件风暴(Event Storming),这种方法简单易学,充分体现了 DDD 中沟通协作和统一语言等要点,所以逐渐开始流行起来。
事件风暴主要分为三步:
- 第一步是把识别领域事件。领域事件表示的是业务流程中每个步骤引发的结果。事件风暴的作者认为,从结果入手来梳理需求,比从操作入手,更容易把业务想清楚。事件风暴中的“事件”两个字就来源于领域事件。领域事件的命名,如果套用英语的语法来说,一般是完成时 + 被动语态。比如说,订单已提交,这个“已”字就是完成时,代表已经发生的事情。而订单已提交也可以说成订单已“被”提交,实际是被动语态,只不过一般把被字给省掉了。除过识别领域事件以外,也要将发现的领域规则写在便利贴上进行备忘,并贴在对应的领域事件的下方。
- 第二步是识别命令。所谓命令就是引发领域事件的操作,我们可以通过分析领域事件得到,将命令贴到领域事件的上方。除了识别出命令本身以外,我们通常还要识别出谁执行的命令,以及为了执行命令我们要查询出什么数据,将执行者和查询数据贴到命令的上方。对于执行者,可以是人(其实是人扮演的角色),也可以是其他系统,还可以是定时器。
- 第三步是识别聚合。先挪动上一步的便利贴,把围绕同一个领域名词的命令、事件、执行者、查询数据摆在一起,分成好几“堆”,然后把领域名词写在大一点的便利贴上,贴在每堆便利贴的中间,这就将围绕同一个领域名词的所有元素聚集在一起,形成聚合。这里的聚合仅仅是领域名词,不同于 DDD 中的聚合。领域名词有可能只是 DDD 聚合充当的角色,或者是 DDD 聚合的属性,还有些领域名词需要经过合并或拆解后,才是合理的 DDD 聚合,而这些需要等到领域建模时才能真正搞清楚。
事件风暴不仅能帮助我们挖掘领域需求,尽量把遗漏的需求补充完全,而且还能以协作的方式保证业务人员和技术人员对需求理解一致。一定要注意,“协作”才是事件风暴的精髓,而具体结果怎样呈现,反而是第二位的。
事件风暴结束后,我们要将过程中建立的统一语言以表格的形式记录下来,包括领域名词、领域动词和领域规则。
领域名词记录举例:
名词(英文/缩写) | 名词(中文) | 解释 | 生命周期 | 关系 |
---|---|---|---|---|
fund | 资金 | 账户中以货币表示的资产 | 开始时创建,销户时注销 | 归属于账户(1:1) |
领域动词记录举例:
动词(英文/缩写) | 动词(中文) | 解释 | 主语 | 宾语 |
---|---|---|---|---|
transfer to | 转账 | 将资金从一个账户转到另一个账户 | 爸爸 | 女儿 |
领域规则记录举例:
规则编号 | 规则描述 | 举例 | 影响的主要功能 |
---|---|---|---|
R001 | 单笔转账限额为 N 万元 | 当 N 为 50万元时:a.如果爸爸给女儿单笔转账 60 万元,则转账失败,错误为超过单笔转账限额; b.如果爸爸给女儿单笔转账 50 万元,则转账成功 | 转账 |
有了统一语言后,我们就可以构建领域模型了。领域模型是统一语言的软件模型,需要通过领域建模得到,在 DDD 中对应模型驱动设计(Model-Driven Design),分为战略设计和战术设计两部分。
面纱四:战略设计
战略设计,这个名词看起来有点高大上,是模型驱动设计的高层设计,而战术设计则是低层设计。如果不以战略设计开始,战术设计将无法被有效实施。在展开具体实现细节之前,需要优先完成宏观层面的战略设计,它强调的是业务战略上的重点,如何按重要性分配工作,以及如何进行最佳整合。
一提起战略设计,很多人就会想起子域(Subdomain)、限界上下文(Bounded Context,BC)和上下文映射(Context Mapping)等模式,其中子域是为了将不同的业务区分开来,也就是要将识别出来的业务概念做一个划分,而限界上下文和上下文映射是将划分出来的业务落实到真实的解决方案中。
领域建模首先是一个定义问题的方法,其次才是解决问题的方法。就是说,一方面,我们要知道解决的问题是什么;另一方面,我们要知道怎么去解决问题。
我们要解决的问题就是领域问题,在 DDD 中,通过子域模式先把问题从大面上进行分解。软件研发是解决问题,而解决问题要分而治之。所谓分而治之,就是要把问题分解了,对应到 DDD 中,就是要把一个大领域分解成若干个小领域,而这个分解出来的小领域就是子域。
通过事件风暴,我们建立了统一语言,而统一语言对应着模型。在战略设计中,我们要对这些统一语言做个分类,把它们划分到不同的子域中去。比如我们要做一个代码打靶服务,首先要把打靶和靶场管理这两件事分开,一个是打靶人员的打靶活动,另一个是代码专家的内容管理。同时,用户登陆和分权分域也是要考虑的基本问题,那么身份管理也可以作为一个子域。类似的,还有数据服务和智能学习等子域。
对于一个真实项目而言,划分出来的子域可能会有很多,但并非每个子域都一样重要。所以,我们还要把划分出来的子域再做一下区分,分成核心域(Core Domain)、支撑域(Supporting Subdomain)和通用域(Generic Subdomain)。
核心域是整个系统最重要的部分,是整个业务得以成功的关键。关于核心域,Eric 曾提出过几个问题,帮我们识别核心域:
- 为什么这个系统值得写?
- 为什么不直接买一个?
- 为什么不外包?
如果你对这几个问题的回答能够帮你找到这个服务或系统非写不可的理由,那它就是你的核心域。对于代码打靶服务来说,打靶子域和靶场管理子域是核心域。
有一些子域不是你的核心竞争力,但却是系统不得不做的东西,市场上也找不到一个现成的方案,这种子域就是支撑域。对于代码打靶服务来说,打靶人员的能力画像、弱项分析和成长曲线等是和打靶结果密切相关的,同时要针对性的建设知识库,并进行智能推荐,让打靶人员更好更快的提升 Code Review 能力和编码能力,这是对代码打靶服务核心能力扩展的重要一步,所以智能学习子域就是一个支撑域。
还有一种子域叫通用域,就是行业里通常都是这么做,即便不自己做,也并不影响你的业务运行。对于代码打靶服务来说,数据服务提供度量看板,包括组织看板和个人看板,行业里有现成的做法,所以数据服务子域就是一个通用域。
我们之所以要区分不同的子域,关键的原因就在于,我们可以决定不同的投资策略。核心域要全力投入,支撑域次之,通用域甚至可以花钱买服务。
通过划分子域,区分核心域、支撑域和通用域,我们把 DDD 在问题层面的概念已经说清楚了。接下来,就要进入到解决方案层面了。我们现在有了切分出来的子域,怎样去落实到代码上呢?首先要解决的就是这些子域如何组织的问题,是写一个程序把所有子域都放在里面呢,还是每个子域做一个独立的应用,抑或是有一些在一起,有一些分开。这就引出了领域驱动设计中的一个重要的概念,限界上下文。
限界上下文,顾名思义,它形成了一个边界,一个限定了通用语言自由使用的边界,一旦出界,含义便无法保证。关于限界上下文的划分,需要同时考虑业务边界、工作边界和技术边界,笔者之前写了一篇文章《聊聊服务化架构的边界》,感兴趣的读者可以进一步阅读。
有了对限界上下文的理解,我们就可以把整个业务分解到不同的限界上下文中,但是,尽管我们拆分了系统,它们终究还是一个系统,免不了彼此之间要有交互。所以,我们就要有一种描述方式,将不同限界上下文之间交互的方式描述出来,这就是上下文映射。DDD 给我们提供了一些描述这种交互的方式,比如:
- 合作关系(Partnership);
- 共享内核(Shared Kernel);
- 客户-供应商(Customer-Supplier);
- 跟随者(Conformist);
- 防腐层(Anticorruption Layer);
- 开放主机服务(Open Host Service);
- 发布语言(Published Language);
- 各行其道(Separate Ways);
- 大泥球(Big Ball of Mud)。
之所以有这么多不同的交互方式,主要是为了让你在头脑中仔细辨认一下,看看限界上下文之间到底在以怎样的方式进行交互。
在定义好不同的限界上下文,将它们之间的交互通过上下文映射呈现出来之后,我们就得到了一张上下文映射图(Context Map)。上下文映射图是可以帮助我们理解系统的各个部分之间,是怎样进行交互的,帮我们建立一个全局性的认知,而这往往是很多团队欠缺的。
面纱五:战术设计
对于战术设计,就是要得到一个具体的模型,这个模型是关于统一语言的软件模型,称作领域模型。经过战略设计,我们已经将统一语言拆分到不同的限界上下文中了,那么领域模型就不是单一的、内聚的和全功能式的模型,而是存在于限界上下文这个显式的边界之内。
领域模型通过领域模型图表达,一般用 UML 类图来画:
我们可以通过对象组合来表达更复杂的业务概念,同时通过对象泛化来表达更抽象的业务概念。建立领域模型,主要是识别领域对象,建立领域对象之间的关系,以及确定领域对象的关键属性,必要的时候还要将领域对象组织成模块。
如果你想把 UML 学的更深入一点,可以读一读《UML 用户指南》和《UML 精粹》。
对于领域对象,DDD 中主要包括实体(Entity)和值对象(Value Object)。实体指的是能够通过唯一标识符标识出来的对象,有生命周期管理,而值对象仅仅表示一个值。实体的属性是可以变的,只要标识符不变,它就还是那个实体。但值对象的属性却不能变,一旦变了,它就不再是那个对象,所以,我们会把值对象设置成一个不变的对象。
在 DDD 中我们为什么要将领域对象分为实体和值对象?其实主要是为了分出值对象,也就是把变的对象和不变的对象区分开。
我们通常会花很大的精力来区分实体和值对象,那么得到的值对象能带来什么好处呢?这就需要了解值对象有什么优点。关于值对象,优点主要体现在内存和数据库布局的灵活性上。有了这种灵活性,就可以根据性能和编程方便性等因素,决定值对象的不同实现方式。其次,值对象的不变性也会带来更高的程序质量。这些优点,都是实体不具备的。
接下来,我们将注意力转移到领域对象的生命周期管理:
- 使用工厂(Factory)模式来创建和销毁领域对象;
- 使用聚合(Aggregate)模式来封装领域对象;
- 使用仓储(Repository)来查找和持久化领域对象。
工厂和仓储理解起来一点都不难,我们重点看一下聚合。
聚合就是多个实体或值对象的组合,它们共同构成了一个业务边界。聚合里可以包含很多个对象,每个对象里还可以继续包含其它的对象,就像一棵大树一层层展开。但重点是,这是一棵树,所以,它只能有一个树根,这个根就是聚合根(Aggregate Root)。聚合根必须是一个实体,是从外部访问这个聚合的起点。可见,最简单的聚合仅包含一个实体。
有了聚合模式后,我们所说的领域对象大多数情况下特指的是聚合,也有时指的是聚合内部的实体或值对象,这个可以通过所在的上下文来判断。
在 DDD 中,关于聚合设计有四条基本规则:
- 在聚合边界内保护业务规则不变性;
- 聚合要设计得小巧;
- 只能通过标识符引用其他聚合;
- 使用最终一致性更新其他聚合。
聚合最基本的作用,是为一组具有整体部分关系的领域对象维护不变规则。当我们掌握了这种建模技术以后,还可以发现其他一些层面的作用:
- 聚合不仅是“被动地”实现不变规则,它还为我们提供了一个新的视角,可以更细致地和业务人员讨论业务规则。
- 技术人员过去一般认为事务只是一个技术概念。现在我们可以看到,事务其实是来源于业务规则的,本质上是个业务问题。也就是说,聚合在业务规则和事务之间建立了起联系。
- 我们在模型上可以为每个聚合建一个包,可以认为,聚合是一种特殊的模块。这样,模型的层次就变得更清晰了。同时,我们也可以把聚合当作一个粗粒度的概念单位进行思考,降低了认知负载。
如果限界上下文中聚合比较多,还可以再通过模块(Module)模式对聚合进行分组,进一步降低认知负载。
面纱六:分层架构
分层架构的一个重要原则是每层只能与位于其下方的层发生耦合。分层架构可以简单分为两种,即严格分层架构和松散分层架构。在严格分层架构中,某层只能与位于其直接下方的层发生耦合,而在松散分层架构中,则允许某层与它的任意下方层发生耦合。
分层架构的好处是显而易见的:
- 首先,由于层间松散的耦合关系,使得我们可以专注于本层的设计,而不必关心其他层的设计,也不必担心自己的设计会影响其它层,对提高软件质量大有裨益。
- 其次,分层架构使得程序结构清晰,升级和维护都变得十分容易,更改某层的具体实现代码,只要本层的接口保持稳定,其他层可以不必修改。即使本层的接口发生变化,也只影响相邻的上层,修改工作量小且错误可以控制,不会带来意外的风险。
DDD 中强调领域模型对应的代码应该是被严格隔离出来的,并保持与模型的高度一致性。在限界上下文中,推荐使用分层架构或者解耦更纯粹的六边形架构,将领域模型和其它支撑代码(UI,DB,通讯等)进行解耦,可以凸显领域模型,有效地管理业务复杂度和技术复杂度。
六边形每条不同的边代表了不同类型的端口,端口要么处理输入,要么处理输出。对于每种外界类型,都有一个适配器与之对应,外界通过应用层 API 与内部进行交互。上图中有 3 个客户请求均抵达相同的输入端口(适配器 A、B 和 C),另一个客户请求使用了适配器 D 。假设前 3 个请求使用了 HTTP 协议(浏览器、REST 和 SOAP 等),而后一个请求使用了 AMQP 协议(比如 RabbitMQ)。端口并没有明确的定义,它是一个非常灵活的概念。无论采用哪种方式对端口进行划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后端口将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。
应用程序通过公共 API 接收客户请求,使用领域模型来处理请求。我们可以将仓储的实现看作是持久化适配器,该适配器用于访问先前存储的聚合实例或者保存新的聚合实例。正如图中的适配器 E、F 和 G 所展示的,我们可以通过不同的方式实现资源库,比如关系型数据库、基于文档的存储、分布式缓存或内存存储等。如果应用程序向外界发送领域事件消息,我们将使用适配器 H 进行处理。该适配器处理消息输出,而上面提到的处理 AMQP 消息的适配器则是处理消息输入的,因此应该使用不同的端口。
我们在实际的项目开发中,不同层的组件可以同时开发。当一个组件的功能明确后,就可以立即启动开发。由于该组件的用户有多个,并且这些用户的侧重点不同,所以需要提供多个不同的接口。同时,这些用户的认识也是不断深入的,可能会多次重构相关的接口。于是,组件的多个用户经常会找组件的开发者讨论这些问题,无形中降低了组件的开发效率。
我们换一种方式,组件的开发者在明确了组件的功能后就专注于功能的开发,确保功能稳定和高效。组件的用户自己定义组件的接口(端口),然后基于接口写测试,并不断演进接口。在跨层集成测试时,由组件开发者或用户再开发一个适配器就可以了。
在 DDD 分层架构中,比较难区分的是领域服务和应用服务,两者都是无状态的。
在面向对象的设计中,有贫血模型和充血模型之分,简单来说:
- 对于贫血模型,对象中仅有数据没有行为,偏向过程式编程范式;
- 对于充血模型,对象中既有数据又有行为,偏向面向对象编程范式。
如果使用贫血模型,则领域对象的行为都会放到领域服务中,这时领域服务就比较多;如果使用充血模型,则领域对象的行为都会封装在内部,这时领域服务就比较少,仅仅包括不适合放到领域对象中的业务行为,比如转账业务行为涉及两个领域对象,一个是源账户,一个是目标账户。
领域层封装的逻辑通常是细粒度的,并不适合直接作为 API 暴露给外部。另外,还有一些不属于领域层的横切关注点,比如像事务控制,应该单独处理。所以,我们往往要在领域层外面再加一层,DDD 分层架构和六边形架构都将这一层称为应用层。 应用层本身并不包含领域逻辑,而是对领域层中的逻辑进行封装和编排。
应用层作为领域层的“门面”,把领域层封装成更粗粒度的服务供外部使用,并且处理事务和日志等横切关注点。应用层的应用服务是必须有的,对应一个具体的用户故事,具有业务价值。但领域层的领域服务可以没有,除非有跨领域对象的业务行为封装。在应用服务中,可以直接调用聚合的接口进行领域对象的创建、查询和持久化等,无需在中间再封装一层冗余的领域服务。
面纱七:两关联一循环
DDD 研发过程如下图所示:
简单说明一下:
- 业务人员(业务方)梳理需求,并向技术人员(技术方)澄清;
- 业务人员和技术人员一起通过事件风暴捕获行为需求,消化领域知识,定义统一语言,同时尝试用统一语言来描述需求;
- 业务人员和技术人员一起完成战略设计和战术设计,完善统一语言,更深刻的理解业务,同时确定分层架构规范;
- 技术人员的代码实现要受到领域模型和分层架构规范的约束,分离业务和技术的关注点,凸显领域模型,同时基于面向模型的实现模式来准确的表达领域模型,让模型和代码一一映射;
- 领域的业务需求“驱动”出了代码实现,同时代码实现又“交付”了领域的业务需求,它们形成了一个正向反馈的循环,可以反复交替,迭代改进,使代码的实现复杂度逐步逼近业务的本质复杂度。
我们可以进一步把 DDD 研发过程精简为两关联一循环:
- 领域模型与统一语言关联;
- 领域模型与代码实现关联;
- 领域模型的演进和精炼循环。
可见,DDD 的核心是领域模型,修改模型就是修改代码,修改代码就是修改模型。
我们都知道,软件研发的核心难度在于处理隐藏在业务知识中的复杂度,那么模型就是对这种复杂度的简化与精炼。所以从某种意义上说,Eric 倡导的 DDD 是一种模型驱动的设计方法:通过领域模型来捕获领域知识,使用领域模型构造更易维护的软件。
起初,不要太在意获得模型是否完美,是否在概念上足够抽象,是否使用了设计模式等。反而,我们更应该关注该如何围绕模型,建立有效的沟通与反馈机制,形成两关联一循环过程。理想的模型,需要是所有人都能懂的模型,而不是满是完美的模式和抽象的概念。
团队通过迭代改进法,不求一步到位,但求一次比一次好。通过两关联一循环,技术人员与业务人员在不断地交流与反馈中,逐步完成对模型的演进和精炼。无论起点多么低,只要能够持续改进,总有一天会有好结果的。而能够支撑持续改进基础的,则是实现方式与模型方式的一致。所以比起模型的好坏(总是会改好的),关联模型与代码实现就变得更为重要了。
由此我们可以更好地理解,DDD 并不是一种编码技术,或是一种特定的编码风格,而是软件研发的一种方法,贯穿软件生命周期的全过程,需要业务人员和技术人员的高效协同。
经常有 DDD 学员问笔者,技术人员如何搞 DDD?笔者一般都会告诉他,要搞 DDD,必须有业务人员参与,只有技术人员参与的 DDD 是伪 DDD。业务人员和技术人员围绕领域模型,建立有效的沟通与反馈机制,形成两关联一循环过程,这才是真正的 DDD,也是 DDD 能真正发挥威力的原因。
小结
本文尝试揭开 DDD 的神秘面纱,总共七层:
- DDD 发展了 OO,抛弃了将分析模型与设计模型分离的做法,而是寻找单个领域模型来满足两方面的要求;
- DDD 发展了敏捷,它显式的把领域和设计放到了软件研发的核心,业务人员和技术人员被得到同样的重视,他们通过协同来构建并演进领域模型;
- 统一语言是业务人员和技术人员使用事件风暴实践共同创建的一套语言,必须在团队范围内达成一致,将技术人员的思考起点拉到了业务上,填平了业务人员和技术人员之间那道人为的鸿沟;
- 战略设计属于高层设计,目标是分离子域,拆分限界上下文,并确定上下文映射,需要优先完成;
- 战术设计属于低层设计,目标是得到一个简单自洽的领域模型,既易于理解,又可以低成本响应需求的变化;
- 分层架构将领域模型和其它支撑代码进行解耦,可以凸显领域模型,有效地管理业务复杂度和技术复杂度;
- 两关联一循环是 DDD的内核,也是 DDD 能真正发挥威力的原因。
希望读者通过本文可以快速俯瞰 DDD 的全貌和内核,从而降低 DDD 的理解成本,提高 DDD 的实践收益。