8

领域驱动实战思考(三):DDD的分段式协作设计

 4 years ago
source link: https://huhao.dev/posts/61190ae2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在我的上一篇文章中,给大家介绍了我在实践中对于DDD设计过程进行梳理的思考。本篇则是向大家整体介绍一下我的“DDD分段式协作设计”的步骤和内容。

同时,该方法的基准化操作手册,也在曾经的一篇文章中公开提供了下载,可以作为更细化的内容进行参考和使用。

由于DDD的相关概念和设计方法极多,相比其他市场上所能见到的操作手法,我在这里向大家所介绍的方法,更加聚焦如何能够确保达到“绝大多数人的60分”, 而非“极少数人的100分”,也就是说,我更加注重的是:

  • 步骤间的连贯性
  • 方法的可操作性
  • 实践的可落地性
  • 与新技术的匹配性(例如云原生)

为此,我尽可能的通过实战检验,在一些需要凭借经验进行综合判断的场景,尽可能的提供虽然不完美但是可以降低选择成本的唯一选项或解释,从而争取让一线实施人员避免“二选一”或“看具体情况再说”这样莫能两可的纠结。

需要说明的是,不同的咨询师在实施DDD的设计过程中手法都不一样,我仅是从我所实施过的咨询项目出发,提供了一种经反复验证可工作的方式,并不代表本方法是唯一正确的。

在这里仅供参考,也欢迎大家进行交流。

DDD解决的问题和办法

在介绍分段式设计之前,让我们回顾一下DDD希望解决的问题:

软件核心复杂性

所谓“复杂”,我根据实际观察总结的理解是:

  • 业务场景多
  • 业务流程长
  • 业务概念多

而具备以上这种特征的业务问题,其复杂度往往都会超出任何单独一个人的大脑所能够理解和处理的范围。

在过去的单体架构时代,由于业务复杂度和技术复杂度都还处于与今天相比更加“简单”的阶段,所以很多时候,我们可能能够依赖少数“聪明的”系统架构师或者“十倍工程师”来通过“拍脑袋”解决问题。

而在今天,这个迈向“第四次工业革命”的时代,当“唯一不变的就是变化”所带来的高业务响应力要求,使得业务问题的复杂度越来越高,而技术复杂度也随着云原生和微服务架构产生了几何增长,甚至催生出了DevOps这样的运动时。对于一个要应对复杂业务挑战的大规模企业来说,依赖少数“聪明人”通过“拍脑袋”来解决问题则变得越来越不现实。

那怎么办才好呢?

这时候,我们需要做的事情,就是 通过集体的力量来解决问题:

  • 通过协作的方式消除部门墙和角色墙,共同应对和分析复杂业务问题,设计解决方案,避免少数人“拍脑袋”;
  • 通过领域驱动的方式,依据业务问题的边界(业务变化的边界)、人员沟通一致性的边界(概念变化的边界)以及弹性伸缩的边界(云原生需求的边界)来架构软件系统,实现业务架构和技术架构相匹配,从而通过业务变化来拉动架构,通过架构来响应业务变化。

这种设计方式所提倡的方法,与过去的“传统设计方法”(嗯,没错,说的就是瀑布式或者伪敏捷)的区别,如下图所示:

IJjeqmi.png!web

而聚焦到DDD本身,DDD首先强调的就是我们应当从过去一上来就先考虑“数据模型和数据库表怎么设计”这种“面向技术进行架构”的方式,改为优先考虑“我们要解决的问题是什么”这种“面向业务进行架构”的方式。

为此,Eric Evans提出了如下三个DDD的核心原则,原文(《Domain-Driven Design Reference: Definitions and Pattern Summaries》)和我的解释图如下:

  1. Focus on the core domain. (聚焦核心域)
  2. Explore models in a creative collaboration of domain practitioners and software practitioners. (领域专家和软件专家通过创造性的协作探索模型)
  3. Speak a ubiquitous language within an explicitly bounded context. (利用明确且有边界的上下文统一语言)

j6BRjqf.png!web

而在DDD思想出现之前,人们进行基于面向对象思想(OO)的系统架构设计的时候,更多的是通过“用例分析法 + SOLID原则 + UML”的方式,基于业务描述中的名词和动词,利用近似“拍脑袋”和“凭经验”的方式来进行建模的,相信但凡长期从事面向对象设计的同行都会有所体会。

而在DDD思想出现和逐渐发展后,该思想使得人们能够从业务抽象、统一语言和问题域划分等多维度进行“更有套路”的面向对象分析,所以DDD被业内人士评价为: OO Done Right (正确的完成面向对象设计),如下图所示:

emYZnmr.png!web

DDD的分段式协作设计

在回顾了DDD所解决的问题和办法之后,让我们来看一看如何通过分段式的协作设计来驱动DDD在设计阶段的落地。

在我所使用的方法中,核心思想是:

通过协作设计,从问题澄清和统一共识出发,逐层递进和细化,实现从业务抽象,到领域建模,再到技术实现的阶段性落地。

由此思想出发,我将分段式设计过程拆分为以下几个阶段:

  • 战略设计阶段: 业务驱动,忽略技术实现细节,为系统架构设计提供业务边界对齐、弹性边界对齐和投资优先级的信息输入。
  • 战术设计阶段: 基于战略设计的信息输出,进行进一步的抽象设计,作为战略设计和技术实现的过渡阶段,为技术实现提供基于抽象模型和业务服务划分的输入。
  • 技术实现阶段: 基于战略设计和战术设计的信息输出,进行技术实现相关的设计,例如:技术性组件补全,技术选型,业务API详细设计,分层架构设计,领域模型类设计,数据库设计,制定测试策略……等等。

接下来,我来分别说一说各个阶段的内容。

战略设计

在战略设计阶段,我们优先关注的是: 开发团队对于业务问题理解上的一致性

在这个过程中,“开发团队”指的是包含了“领域专家”在内的所有软件开发相关的关键角色,例如:客户、产品经理 / 项目经理、用户体验设计师、系统架构师、开发工程师、测试工程师、运维工程师、数据库管理员、安全专家……等等。

而“领域专家”,则是一个能力上的称呼,而非职务上的某种角色,“领域专家”指的是: 对需要解决的业务问题具备最丰富的领域知识,能够帮助和指导开发团队进行分析和设计的那个人(也可能是多个人的组合)

之所以需要这么完备的人员构成,是因为协作设计的核心是需要将设计过程“前置(Shift Left)”到软件开发价值流的早期,通过深入的碰撞、讨论来形成 共识 ,通过共识驱动整个开发的价值流,减少因为缺乏共识导致的扯皮、返工等浪费。

在该阶段,这些人聚在一起, 忽略技术实现细节 ,通过通力协作关注以下几件事:

  • 业务梳理和抽象: 通过事件风暴工作坊,对现实业务流程进行以系统实现为目的的抽象。
  • 限界上下文识别: 通过统一语言(消除语言二义性),对抽象概念进行澄清、分类和查漏补缺,从而识别业务边界。
  • 问题子域识别 :通过对问题域进行识别和澄清,划分问题边界和问题域类型,对架构设计提供投资优先级的参考。

业务梳理和抽象

首先,团队需要保证所有人对于业务所要解决的问题、业务场景和业务流程的理解是一致的,而这一点在实际中恰恰是绝大多数团队最为明显的“软肋”。在现实中,每个人脑子里面对于业务的理解都是不一致的,而每个人又无法看到对方的理解长什么样。所以,我们需要通过一种可视化的协作设计方式,利用不同颜色的便利贴、马克笔在大白纸上进行沟通和交流(俗称“糊墙”,我们这些DDD顾问经常自嘲为“糊墙师”)。

业务抽象可视化的方式有很多种,其中比较著名的是 “四色建模(Color Modeling in Color)”“事件风暴(Event Storming)” 。在这里,我所使用的是入门门槛更低,更“傻瓜”的事件风暴。

事件风暴是一种用于DDD的协作设计方法。该方法基于现实业务流程,以系统实现为视角,基于领域事件的发生时间线,通过一次只关注一个维度(分离关注点)的分层抽象方式,将现实业务流程进行抽象并转化为系统实现的业务逻辑,这些步骤包括:

  • 识别领域事件
  • 识别决策命令
  • 识别领域名词

通过使用事件风暴对业务进行梳理和抽象,能够统一团队认知,并为战术设计阶段的领域建模提供直接输入。

PS:我所使用的事件风暴实施过程,由于经过了大量的实战检验、总结和持续调整,已经与事件风暴创造者Alberto Brandolini先生的现有方法大不相同,有关于Alberto版本的事件风暴相关信息,可以 查看他的网站

业务梳理和抽象的产出物,如下图所示:

IRB7vaB.png!web

限界上下文识别

在业务梳理和抽象完成后,团队接下来需要做的,是将事件风暴工作坊过程中产出的领域名词和外部系统拿出来,根据概念的相关性和理解上的“二义性”,将它们分门别类,按照“限界上下文(Bounded Context)”划分清楚他们的概念边界。

限界上下文,是概念的边界,在该边界内,当我们去交流某个业务概念时,不会产生理解和认知上的歧义(二义性),限界上下文是统一语言的重要保证,同时也是业务问题最小粒度的划分。

然后,团队需要根据限界上下文间概念的依赖关系,对限界上下文进行进一步分析,画出它们之间的依赖关系,以便发现和识别一些典型的“设计坏味道”,例如:

  • 双向依赖:上下文之间缺少一层未被澄清的上下文,或者两个上下文其实可被合为一个;
  • 循环依赖:任何一个上下文发生变更,依赖链条上的上下文均需要改变;
  • 过深的依赖:自身依赖的信息不能直接从依赖者获取到,需要通过依赖者从其依赖的上下文获取并传递,依赖链路过长,依赖链条上的任何一个上下文发生变更,其链条后的任何一个上下文均可能需要改变;

通过对于限界上下文的划分和依赖关系的识别,团队能够实现软件架构在概念边界上的内聚和解耦。

PS:我使用了限界上下文依赖关系,代替了人们常用的“限界上下文映射(Context Mapping)”,因为限界上下文的7种上下游映射关系,所反映的是团队间的各种协作关系,这一步是一个非常细化的分析过程,在战略设计这种宏观分析阶段,实际中非常难以提前识别和分析,因为很多时候团队都还没有。而绝大多数情况下,我们做限界上下文分析的时候,都是为了能够快速的指导系统业务模块或服务的划分,所以我从 C4模型(C4 Model) 中找到了灵感,进行了简化和代替。

限界上下文识别过程的产出物,如下图所示:

aaY3uej.png!web

问题子域识别

在战略设计阶段的最后,团队需要根据限界上下文识别的产出,按照“一个子域负责解决具有一个独立业务价值的问题”的原则,将限界上下文以“切蛋糕”的方式,划分到不同的问题子域(Subdomain)中,并按照以下三种不同的子域类型进行标注:

  • 核心域(Core Domain): 是当前产品的核心差异化竞争力,是整个业务的盈利来源和基石,如果核心域不存在,那么整个业务就不能运作。对于核心域,需要投入最优势的资源(包括能力高的人),和做严谨良好的设计。
  • 通用子域(Generic Subdomain): 该类问题在界内非常常见,所以很可能有现成的解决方案,通过购买或简单修改的方式就可以使用。
  • 支撑子域(Supporting Subdomain): 该类问题解决的是支撑核心域运作的问题,其重要程度不如核心域,又不属于通用子域,具备强烈的个性化需求,难以在业内找到现成的解决方案,需要专门的团队定制开发。

问题子域,是对业务问题的澄清和划分,同时也是对于资源投入优先级的重要参考,相对限界上下文来说,是对业务问题更大粒度的划分。

通过对于子域进行识别、划分和类型标注,团队能够实现软件架构在业务边界上的内聚和解耦。

PS:在DDD的概念中,限界上下文和问题子域是两个不同维度的概念,理论上来说并没有相互的依赖关系,为了能够方便操作和降低落地成本,依据实践效果,我刻意的选择了“一个子域包含多个限界上下文,一个上下文不得存在于多个子域”的方式。

问题子域识别过程的产出物,如下图所示:

quMR7ri.png!web

战术设计

在战术设计阶段,我们优先关注的是: 通过抽象模型和业务模块划分来承载业务抽象,为DDD从战略到实现进行过渡

这时候,团队已经在战略设计阶段完成了对于业务问题的澄清和抽象,需要深入建模细节,探讨软件架构的更细粒度的设计。

在该阶段,开发团队需要 继续忽略技术实现细节 ,做以下几件事:

  • 领域建模: 针对核心域内的业务概念进行领域建模,通过聚合、实体、值对象的识别,为技术实现中的面向对象设计提供参考。
  • 业务服务划分: 基于战略设计的输出,结合“云原生思想”、“康威定律”和“逆康威定律”,划分具体的业务服务单元。
  • 业务服务接口能力识别: 根据领域建模和业务服务划分的输出,确定每一个业务服务单元对外暴露的“必要”接口能力清单(忽略具体的协议、地址和数据结构)。

领域建模

领域建模,是通过将业务抽象为以下几种抽象模型的方式,利用模型承载和响应业务的变化:

  • 聚合(Aggregate): 负责封装业务逻辑,通过一致性边界和统一语言,内聚决策命令和领域事件,容纳并识别领域名词为以下不同的抽象模型:
    • 实体(Entity): 是聚合的主干,具有唯一标识和生命周期。
    • 聚合根(Aggregate Root): 是一种实体,是聚合的根节点。
    • 值对象(Value Object): 是实体的附加业务概念,用来描述实体所包含的业务信息。

以上抽象模型,同属领域模型(Domain Model),是对业务的高度抽象,利用抽象模型作为业务和系统实现的核心联系,领域模型封装和承载了全部的业务逻辑,并通过聚合的方式保持业务的“高内聚,低耦合”。

在后续的技术实现过程中,聚合就是一种文件目录结构(例如:包、命名空间、模块),里面存放了领域模型相关组件及其他的领域层组件,例如:领域服务(Domain Service),工厂(Factory),仓储接口(Repository)等。

领域建模中的聚合,在承载业务逻辑的同时,是对业务问题最细粒度的澄清和划分,一个限界上下文可能包含多个聚合,一个聚合不能存在于多个限界上下文。

通过领域建模和对聚合的设计,团队能够实现软件架构在模型层面上的内聚和解耦。

领域建模过程的产出物,如下图所示:

rA3aErz.png!web

业务服务识别

业务服务识别,是为后续系统实现进行的基于业务边界的模块拆分分析,业内常见的拆分方法有:

  • 基于限界上下文进行拆分: 每个限界上下文为一个服务,优点是每个服务都很小,代码量少;缺点是拆分粒度太细,导致服务数量过多,增加架构设计的复杂度和运维成本。
  • 基于子域进行拆分: 每个子域为一个服务,优点是服务数量相对较少,架构复杂度和运维成本相对更低;缺点是拆分粒度在某些场景下会非常大,导致单个服务变成“小单体”,增加开发成本和代码分层复杂度。
  • 基于弹性边界进行拆分: 这个方式是云原生时代新的拆分方式,通过针对服务实例的弹性伸缩的功能性需求或非功能性需求,以弹性边界为决定性参考,结合子域和限界上下文的分析,进行模块拆分。这种方式的优点是,服务粒度和数量适中,更贴近实际需要,开发和运维成本均衡;缺点是引入了一个更贴近运营需求和技术实现的参考维度,增加了系统架构的能力要求和复杂度(这种方法我取自于ThoughtWorks中国区CTO徐昊)。

从我的实战检验来看,我刻意的选择“基于弹性边界进行拆分”的方式,因为这种方式的说服力更高,性价比也最高,至于对于架构能力和复杂度的要求嘛……对于一个技术顾问和云原生时代的架构师来说,这应该都不是问题才对……

通过对于业务服务进行划分,团队能够获得对软件架构模块拆分的直接指导,并且还能够依据“逆康威定律”依据架构结果进行开发团队的划分和组建。

业务服务识别过程的产出物,如下图所示:

nU7JB3e.png!web

业务服务接口能力识别

在识别和划分了业务服务之后,我们需要针对每一个业务服务,基于领域建模时聚合的决策命令,补全和定义每一个业务服务对外暴露的接口能力,这种接口能力一般就是两类:

  • 写类型的接口能力
  • 读类型的接口能力

而在这个过程中,由于我们还是处于协作设计的阶段,忽略具体技术实现细节,所以我们并不关心接口的实现方式,例如:接口的设计风格、接口的协议、接口的地址、接口的数据结构、是否是事件驱动、是否用消息队列、是同步接口还是异步接口等。这些东西都是在后续技术实现阶段进行的更详细的设计过程。

通过对业务服务的接口能力进行识别,团队能够提前定义业务服务的接口概要设计方案,为后续负责具体开发的团队提供直接的输入,方便他们进行接口的详细设计。

PS:很多老的DDD设计方式,在这时候往往都会使用Swagger来定义基于RESTful Web API的接口,来实现“API驱动开发(API Driven Development)”。一方面正如前述所说,这都是技术实现细节,提前考虑技术实现细节一方面会增加协作设计阶段的成本,另一方面会干扰领域驱动设计的过程。另一方面,当今的微服务,RESTful Web API这种同步接口的设计,已经不再是最优的默认接口设计风格,内部服务间通过gRPC或消息队列进行通信,对前端服务使用GraphQL等技术实现查询式接口,已经逐渐成为了新的趋势,而RESTful Web API则主要用于对外部服务暴露开放接口的场合。所以,我改为了只对接口能力进行识别,忽略技术实现细节。

业务服务接口能力识别过程的产出物,如下图所示:

iqEZBbU.png!web

技术实现

在完成了战略设计和战术设计之后,需要众多关键角色集中投入的协作设计过程,就告一段落了。剩下的与具体技术要求更紧密的详细设计过程,刻意交由具体负责业务服务开发的团队去进行后续的设计和实现。

技术实现阶段要做的事情,包括且不限于:

  • 补全技术组件:补全为了支撑系统实现的关键技术型组件,例如客户端、BFF(Backend for Frontend)、ACL(Anticorruption Layer,防腐层)等,完善系统应用类组件(需要分清应用和基础设施的区别)。
  • 技术选型:选择适合的技术栈或工具。
  • API详细设计:选择适合的通讯方式、API设计风格和开发框架,利用契约测试或可视化文档对API进行详细设计。
  • 分层架构设计:采用符合领域驱动设计风格(或者其它符合整洁架构思想的)的分层架构思想,设计单个服务的架构。
  • 领域模型类设计:参考领域模型的设计,利用面向对象的语言设计具体的类。
  • 持久化设计:参考领域模型的设计和实际的持久化相关指标,对持久化组件(例如数据库)进行选型和设计。
  • 制定测试策略:针对实际需要和性价比,设计适合的测试策略,以守护架构设计。
  • ……(其它)

需要特别注意的是:哪些东西属于技术实现细节?哪些东西属于抽象业务?这个判断将会贯穿整个DDD的分段式协作设计过程,任何过早进行技术实现细节的思考和讨论,都有极大的风险导致领域驱动走偏。

所以,在实战过程中,我通常采用一个非常有效的方式来提醒大家:

拉一个写着“绝不提前考虑技术实现”的横幅贴在墙上……

总结

以上内容,就是关于我的“DDD分段式协作设计”的介绍,具体的操作步骤和注意事项,欢迎参考 先前文章中提供的操作手册

当然,从2020年起,我也开始组织公开的“领域驱动设计练功房”培训课程(收费课程),在这个培训课程中,我们不但会通过实战的方式让大家体会协作设计的过程,也会通过测试驱动开发(TDD)的方式带领大家体验DDD技术实现过程中有关分层设计的方式。

在培训和线上社群中,我也会更多的分享实战中的案例和经验,为参与的学员答疑解惑:

ra2yUnb.jpg!web

当然,有任何问题和建议,欢迎大家通过文章下方的评论进行交流,同时关注后续文章。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK