书名:领域驱动设计工作坊
ISBN:978-7-115-64134-2
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 郑天民
责任编辑 秦 健
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书通过一个完整项目案例由浅入深地介绍了业务建模和软件设计的方法论—领域驱动设计(Domain Driven Design,DDD)。首先,本书介绍了DDD的基本概念和主流设计方法,同时引入贯穿全书的案例系统,并完成案例系统的基础设计;其次,围绕DDD的统一语言、子域和限界上下文展开讨论,探讨从问题空间进入解空间的解决方案;再次,从领域建模范式讲起,详细分析实体、值对象、聚合、领域服务、应用服务和领域事件等DDD中与战术设计相关的核心概念,并给出各个组件的设计方法和使用技巧;最后,围绕常见领域驱动架构模式及DDD架构考量,给出DDD的架构设计和整合方案。
本书结构清晰、内容丰富、图文并茂,适合团队负责人、业务设计人员、系统设计人员以及架构师等阅读。
对于软件开发,如何将业务问题转变为系统解决方案一直是困扰开发人员和架构师的一大难题。针对这一难题,业界诞生了一批系统建模方法论,其中领域驱动设计(Domain Driven Design,DDD)无疑是当下热门的建模方法之一。随着微服务架构大行其道,DDD已成为构建微服务系统的主流设计思想和模式。
总体来说,DDD提供的是一种开展业务建模和软件设计的方法论。与其他方法论的不同之处在于,DDD强调开发人员与业务领域专家高效协作,共同交付业务价值。从架构设计上说,DDD认为良好的系统架构应该是技术架构和业务领域相互融合的结果,不能脱离业务领域设计技术架构。
基于多年来对DDD的实施和培训经历,笔者充分认识到:虽然DDD能够帮助开发人员更好地完成业务建模和系统架构设计,但它是一种比较复杂的建模方法,其中包含一系列不易理解的概念。而在现实开发过程中实现这些概念,需要引入专门的设计方法和工作开展方式。很多时候,开发人员充分理解DDD中的设计思想和聚合、资源库、应用服务等概念已属不易,更不要说把它们应用到日常开发中。如何让DDD真正在自己负责的项目中落地,是摆在开发人员面前的一大难题。
面对这一难题,最好的解决方案是实践,而最好的实践是动手实操。工作坊(Workshop)就是这样一种可以让开发人员动手实操的学习和训练方式。它注重解决实际问题,通过引导开发人员思考和分析真实案例,提供解决问题的方法和技巧。笔者在实际开发中,通常采用这种方式启动并推进DDD的设计过程。这一过程已被国内外各大公司认为是一项实施DDD的优秀实践。笔者梳理了一套系统化的工作坊实施流程,并设计了一个贯穿整个流程的完整项目案例。
本书内容分为4篇,分别对应工作坊实施过程中的4个阶段。各篇的内容组织如下。
● 基础概念篇,包含第1章和第2章。在该篇中,我们将全面介绍DDD的基本概念和主流设计方法,同时引入贯穿全书的案例系统,并形成案例系统的基础设计,即产出案例系统V1.0。
● 战略设计篇,包含第3章~第6章。在该篇中,我们将围绕DDD的统一语言、子域和限界上下文展开讨论,探讨从问题空间进入解空间的解决方案。在工作坊实践中,我们将采用目前主流的“事件风暴”模式以完成对案例系统业务全景的探索并形成案例系统的第1版设计,即产出案例系统V2.0。
● 战术设计篇,包含第7章~第10章。在该篇中,我们将从领域建模范式讲起,详细分析实体、值对象、聚合、领域服务、应用服务和领域事件等DDD中与战术设计相关的核心概念,并给出各个组件的设计方法和使用技巧。在工作坊实践中,我们将基于战略设计篇中“事件风暴”模式产生的阶段性产物,结合本篇中的各个设计组件以完成对案例系统战略设计和战术设计的整合,从而形成案例系统的第2版设计,即产出案例系统V3.0。
● 架构设计篇,包含第11章和第12章。在该篇中,我们将围绕常见领域驱动架构模式及DDD架构考量,给出DDD的架构设计和整合方案。在工作坊实践中,我们将基于前面各篇中生成的阶段性产物,结合本篇采用的架构设计模式,完成案例系统的最终设计方案并产出案例系统V4.0,也就是最终版本。
本书适合广大业务设计人员、系统设计人员以及架构师阅读和参考。读者不需要具备技术开发能力,也不限于特定的行业和领域,但熟悉产品设计和技术研发工作流程并掌握一定系统设计基本概念有助于更好地理解本书的内容。同时,本书也适合团队管理人员阅读和参考。本书所阐述的DDD方法不仅适用于系统边界划分,同样适用于团队与组织边界的划分和管理,并能够从管理角度给出系统设计的一些指导性建议。
感谢我的家人特别是妻子章兰婷女士,在本书的撰写过程中,在我占用大量晚上和周末时间写作时给予的极大支持和理解;感谢以往和现公司的同事,身处业界领先的公司和团队,笔者得到了很多学习和成长的机会,没有大家的帮助这本书不可能诞生;感谢人民邮电出版社异步社区的编辑团队,你们的帮助使得本书得以顺利出版。
由于时间仓促,加上笔者水平和经验有限,书中难免有欠妥和错误之处,恳请读者批评指正。
郑天民
2024年于杭州钱江世纪城
郑天民,日本足利工业大学信息工程学硕士,拥有十余年软件行业从业经验,目前在一家大健康领域的创新型科技公司担任CTO,负责产品研发与技术团队管理工作。他开发过十余个面向开发人员的技术和管理类培训课程项目,在架构设计和技术管理方面有丰富的经验。他是阿里云MVP、腾讯云TVP、TGO鲲鹏会会员。他著有《Apache ShardingSphere实战》《Spring响应式微服务:Spring Boot 2+
Spring 5+Spring Cloud实战》《系统架构设计》《微服务设计原理与架构》《Spring Security原理与实践》等图书。
本书提供如下资源:
● 配套讲解视频;
● 书中图片文件;
● 本书思维导图;
● 异步社区7天VIP会员。
要获得以上资源,您可以扫描右侧二维码,根据指引领取。
作者和编辑虽然尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区(https://www.epubit.com),按书名搜索,进入本书页面,单击“发表勘误”,输入勘误信息,单击“提交勘误”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是由人民邮电出版社创办的IT专业图书社区,于2015年8月上线运营,致力于优质内容的出版和分享,为读者提供高品质的学习内容,为作译者提供专业的出版服务,实现作者与读者在线交流互动,以及传统出版与数字出版的融合发展。
“异步图书”是异步社区策划出版的精品IT图书的品牌,依托于人民邮电出版社在计算机图书领域四十余年的发展与积淀。异步图书面向各行业的信息技术用户。
随着互联网业务和技术的持续发展,软件系统自身也日益复杂。在现实中,绝大多数软件开发工作都是围绕现实业务问题而展开的,而业务问题的复杂度是软件开发成功的关键因素之一。那么,如何有效应对系统的复杂度?DDD可以帮助人们更好地实现这一目标。
本章首先围绕软件复杂度的概念和表现形式进行深入剖析,继而引出DDD、设计思想和方法。从设计思想上说,DDD为开展系统建模工作提供了一种崭新的模式。而在设计方法上,DDD则在战略设计和战术设计这两大维度上给出了全面的工程实践。借助DDD,我们可以实现从面向业务的问题空间映射到面向技术的解空间,并应对软件复杂度所带来的技术挑战。
当下,DDD应用越来越广泛,无论是传统的单体系统,还是主流的微服务架构或中台架构,都可以从架构模式角度出发与DDD进行融合。在本章的末尾,我们将讨论领域驱动设计与这些主流架构之间的关联关系和融合方法。
任何软件系统的发展都遵循从简单到复杂、从集中到分散的过程。在系统诞生的初期,我们习惯于构建单一、内聚和全功能式的系统,因为这样的系统完全可以满足当前业务的需求。当业务发展到一定阶段,集中化系统开始表现出诸多弊端,功能拆分与服务化思想和实践被引入。而当系统继续演进,团队规模随之增大,由于分工模糊和业务复杂度不断上升,系统架构逐渐被腐化,直到系统不能承受任何改变,进入需要重新拆分的阶段。推倒重来意味着重复从简单到复杂、从集中到分散的整个过程,如图1-1所示。
图1-1 系统架构的轮回
系统架构的轮回给人们的启示就是将所有东西放在一起不是一个好的选择,软件系统的关注点应该清晰划分,并能通过功能拆分降低系统复杂性。因此,在本质上,架构的演进过程就是一个解决系统复杂度问题的过程。那么,软件复杂度具体指的是什么呢?我们又应该如何对复杂度本身进行剖析呢?图1-2给出了问题的答案。
图1-2 软件复杂度的3个维度
在图1-2中,我们可以看到软件复杂度的形成涉及3个维度——规模、结构和变化。本节将详细介绍图1-2中的3个复杂度维度。同时,我们也需要认识到,软件复杂度体现的是一种客观规律,因为任何一个软件系统都会受到这3个维度的影响,开发人员无法完全避免,但引入一定的设计思想和方法可以降低软件复杂度。在本节中,我们将展开介绍软件复杂度的应对策略。
本节将首先讨论第一个软件复杂度维度——规模。规模是软件复杂度最基本的表现形式。
关于如何评估一个软件系统的规模,业界存在很多实践方法。图1-3展示了一种常见的评估方法——功能点(Function Point,FP)评估法。
图1-3 功能点评估法
FP评估法是软件行业专用的估算方法之一。应用FP评估法时,首先识别系统边界和应用类型,区分新开发的系统和增强型遗留系统;然后识别系统的功能点计数项,包括内部逻辑文件数量、对外接口数量、输入和输出数量,以及包括排序和聚集在内的查询数量等五大计数项;识别各个功能点计数项并确定各项指标的系数后,加权求和即得到最终的估算结果。在图1-3中,我们可以看到该示例中的功能点数量为215。从这个示例中,我们明确了软件规模的一种表现形式——数量。
我们再来看软件规模的另一种表现形式——交互。图1-4展示了McCabe圈复杂度(Cyclomatic Complexity)的组成结构。
图1-4 McCabe圈复杂度的组成结构
这里解释一下圈复杂度的概念。如果一段代码中不包含控制流语句(条件或决策点),那么这段代码的圈复杂度为1,因为这段代码中只有一条路径;如果一段代码中仅包含一个if语句,且if语句仅有一个条件,那么这段代码的圈复杂度为2;如果一段代码中包含两个嵌套的if语句,或者一个if语句有两个条件代码块,那么这段代码的圈复杂度为3……以此类推。
在软件测试的概念中,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的路径条数,即合理预防错误所需测试的最少路径条数,路径条数本质上就是系统内部的交互过程。圈复杂度高说明交互过程的复杂性高。根据经验,代码的出错可能性和圈复杂度的高低有很大关系。
如何应对规模导致的软件复杂度?基本思路就是通过分而治之来控制规模。分而治之是一种设计思想,这一设计思想有多种实现策略,其中最具代表性的就是图1-5所示的AKF扩展立方体。
图1-5 AKF扩展立方体结构
AKF扩展立方体是业界关于如何开展系统拆分工作的一条原则,通过这条原则,系统就可以实现高度的扩展性。在AKF扩展立方体的X轴上,开发人员可以使用负载均衡等技术来实现水平复制;在Z轴上,开发人员可以使用类似数据分区的方式实现系统扩展性。这里需要重点关注的是Y轴,它提示针对单体系统,应该基于业务体系按功能进行拆分。实际上,AKF扩展立方体也为拆分微服务提供了解决方案。
系统拆分的基本思路有两种——纵向(Vertical)拆分和横向(Horizontal)拆分。所谓纵向拆分,就是将一个大应用拆分为多个小应用。如果新业务较为独立,那么直接将其部署为一个独立的应用系统。例如,在图1-6中,将互联网医院系统拆分为医生子系统、就诊子系统和患者子系统等独立业务子系统。
图1-6 系统纵向拆分示例
纵向拆分关注业务,基于不同的业务场景,通过将内聚度较高的相关业务进行剥离以形成不同的子系统。相较纵向拆分的面向业务特性,横向拆分更关注技术。将可以复用的业务进行拆分,独立部署为分布式服务后,我们只需调用这些分布式服务即可构建复杂的新业务。所以,横向拆分的关键在于识别可复用的业务,设计服务接口并规范服务依赖关系,示例如图1-7所示。
图1-7 系统横向拆分示例
图1-7是对图1-6中的互联网医院系统进行横向拆分的结果。可以看到,当我们将医生、就诊、处方和患者等业务抽象为独立的垂直化服务,并在各个服务上应用分布式环境下的调用和管理框架时,系统的业务就可以转变为一种排列组合的构建方式,如基于医生和处方服务,我们可以构建出业务A,基于就诊和患者服务,我们可以构建出业务B。
软件规模在很大程度上决定了系统的复杂度,这是一个显而易见的结论。那么,结构为什么也会影响软件复杂度呢?这就是本节要讨论的话题。
关于结构,我们来看一个示例。
图1-8展示了一个常见的分布式架构。我们在图中看到了Web服务与业务服务之间的分离,并引入分布式缓存来提升数据访问性能。更进一步,还使用消息中间件来实现不同业务服务之间的消息通信,从而构建低耦合的系统架构。当然,我们还可以在图中添加搜索引擎、分库分表等技术体系。在现实中,当面临系统架构设计问题时,可以通过引入各种技术系统逐步完善架构,直至构建具有庞大体量的大型集群系统。本质上,软件架构重构的需求和动力来自对系统质量要求的不断提升,如性能需求、解耦需求、搜索查询需求等。因此,结构导致软件复杂度提升的第一个要点是质量。
图1-8 分布式架构
前面讨论的结构指的是技术结构,接下来将讨论组织结构。正如康威定律(Conway’s Law)所指出的,设计系统的组织,其产生的设计和架构等价于组织间的沟通结构。图1-9展示了不同公司所采用的组织结构。
图1-9 不同公司具有不同的组织结构
康威定律无处不在,从传统的单体架构到目前主流的微服务架构实际上都是康威定律的体现。现在很多开发团队本质上是分布式的,单体架构的开发、测试、部署、协调、沟通成本巨大,严重影响效率且容易产生冲突。将单体架构拆分为微服务,每个团队独立开发、测试和发布自己负责的微服务,互不干扰,系统效率得到提升。可见,组织和系统架构之间存在映射关系:一方面,如果组织结构和文化结构不支持,通常无法成功建立有效的系统架构;而另一方面,如果系统设计或者架构不支持,那么无法成功建立一个高效的组织。图1-10展示的就是一个不合理架构设计的示例。
图1-10 不合理架构设计
在图1-10中,我们看到了经典的软件系统三层架构模式,即用户展示层调用业务逻辑层,而业务逻辑层进一步调用数据访问层。显然,每一层的组件应该有明确的职责,用于用户交互和展示的组件不应该包含在业务逻辑层中。但在现实场景下,由于团队或部门之间的岗位职责和工作边界的不清晰,可能会出现图1-10所示的将页面控件放在业务逻辑层中的不良设计。这就是组织所带来的问题导致软件复杂度提升的一个典型案例。
针对结构所引起的软件复杂度,业界也有成熟的处理方案,基本思想是通过软件架构模式保证系统结构清晰有序。
图1-11展示了一种通用的分层结构。分层结构是最基本,也是最常见的架构模式,每一层次之间通过接口与实现的契约方式进行交互,可以严格限制跨层调用,也可以支持部分功能的跨层交互以提供分层的灵活性。典型的三层结构及各种在三层结构上衍生的多层结构就是这种风格的具体体现。
图1-11 分层结构
我们再来看一个更加复杂的分层结构,如图1-12所示。该图展示了一个客服系统的服务架构。整个系统可以分为前台服务层、中台服务层、集成服务及外包客服系统。其中,前台服务层提供面向用户的业务服务,中台服务层提供通用基础服务,而集成服务则用于完成与外部各个外包客服系统之间的有效集成。通过这种分层方式,各个服务层职责明确、各司其职。
图1-12 客服系统的分层架构
介绍完分层结构,我们再来看一个典型的架构模式——管道–过滤器模式。管道–过滤器模式是用于解决适配和扩展性问题的代表性架构模式。管道–过滤器模式在结构上主要包括过滤器(Filter)和管道(Pipe)两种元素,如图1-13所示。过滤器负责对数据进行加工处理。每个过滤器都有一组输入(Input)端口和输出(Output)端口,从输入端口接收数据,经过内部加工处理之后,传送到输出端口。同时,数据通过相邻过滤器之间的连接件进行传输,管道可以看作输入数据流和输出数据流之间的通路。
图1-13 管道–过滤器模式
管道–过滤器模式将数据流处理分为几个顺序的步骤分别进行,一个步骤的输出是下一个步骤的输入,每个处理步骤由一个过滤器来实现。每个过滤器独立完成自己的任务,不同过滤器之间不需要任何交互。这些特性允许将系统的输入和输出看作各个过滤器行为的简单组合,独立的过滤器不仅能够降低组件之间的耦合程度,而且可以很容易地将新过滤器添加到现有的系统之中。同样,原有过滤器也可以很方便地被改进的过滤器所替换,以扩展系统的业务处理能力。
架构模式是一个丰富而复杂的话题,业界存在一大批即插即用的架构模式,这里无意一一列举,读者可以自行参考相关书籍进行系统学习。
软件复杂度与变化之间的关系不言而喻。对于软件开发,变化是永恒的,唯一不变的就是变化本身。既然变化不可避免,那应该如何有效应对变化所产生的影响呢?接下来将围绕该问题展开介绍。
我们可以引用架构设计上的一个核心概念来回答上述问题,这个概念就是扩展性(Extensibility)。所谓扩展性,指的是当系统的业务需求发生变化时,我们对当前系统改动程度的一种控制能力。改动程度越大,扩展性就越低。扩展性低的本质原因在于代码组件之间的边界往往很难清晰划分。图1-14展示了代码组件之间的一个边界场景。
在图1-14中,我们看到了5个代码组件。代码组件是一种泛指,可以指向类、模块、服务、子系统等概念。在图1-14所示的边界之下,当向该系统中添加新业务时,假设只需要开发一个新的代码组件6替换原有的代码组件5,我们就认为系统具有较好的可扩展性。也就是说,不需要改变原有的各个代码组件,只需将新业务封闭在一个新的代码组件中就能完成整体业务的升级,这就是边界的力量。
图1-14 代码组件边界
另一个应对变化的核心手段是抽象。首先将系统中容易发生变化的点抽取出来并形成一个个扩展点,然后对扩展点进行替换就能完成系统的升级。图1-15展示了这样一种运行时的效果图。
图1-15 扩展点和SPI
在图1-15中,我们以SPI(Service Provider Interface,服务提供接口)的形式定义了扩展点。SPI是JDK提供的一种内置机制,是JDK中用来进行插件式管理的扩展点。JDK为SPI的执行提供了一种运行时环境。为了使用SPI,我们需要梳理系统的变化并将它们抽象为一个个SPI扩展点。如果我们能够抽象出合理的SPI扩展点,也就意味着可以合理地应对系统的变化。
应对软件系统的变化的基本思路是通过抽象顺应变化方向,并完成对系统的建模。统一建模语言(Unified Modeling Language,UML)为面向对象软件设计提供统一的、标准的、可视化的建模语言。它适用于描述以用例为驱动,以体系结构为中心的软件设计的全过程。图1-16展示的是商品管理业务场景下的UML用例图。
图1-16 商品管理业务场景下的UML用例图
图1-17展示了一张UML中的类图。通过类图,在定义类的时候,将类的职责分解为类的属性和方法。类在类型上可以分为实体类(Entity)、边界类(Boundary)和控制类(Control),实体类和边界类的划分与本章后续内容中介绍的DDD思想完全一致。映射需求中的每个实体而得到的类称为实体类,实体类保存要进行持久化的信息,而信息需要在用例内、外流动;边界类用于实现信息映射;控制类用于识别控制用例工作的类。
图1-17 UML类图
UML一共提供了9种图形来表述业务场景中各个事物及其关联关系,如表1-1所示。
表1-1 UML图例
名称 |
描述 |
类别 |
模型 |
---|---|---|---|
类图 |
类及类之间的相互关系 |
静态图 |
结构建模 |
对象图 |
对象及对象之间的相互关系 |
||
组件图 |
组件及其相互依赖关系 |
实现图 |
|
部署图 |
组件在各个节点上的部署 |
||
时序图 |
强调时间顺序的交互图 |
交互图 |
行为建模 |
协作图 |
强调对象协作的交互图 |
||
状态图 |
类所经历的各种状态 |
行为图 |
|
活动图 |
工作流程的模型 |
||
用例图 |
需求捕获和描述 |
用例图 |
至此,我们对软件复杂度进行了全面的剖析,图1-18对剖析内容进行了总结。
图1-18 软件复杂度各个维度下的关注点
可以看到,我们分别从规模、结构和变化这3个维度给出了软件复杂度对应的表现形式和关注点。这些表现形式和关注点为引入DDD思想和方法提供了理论基础。同时,架构师使用模型来表述系统,而模型是一个抽象概念,需要借助特定工具和方法进行表述。事实上,DDD也可以看作一种系统建模方法。
从本节开始,我们将正式进入领域驱动设计的世界。我们将从领域驱动设计的基本概念说起,引入面向领域的战略设计和战术设计方法,并尝试通过领域驱动设计来解决软件复杂度问题。在后续内容中,为了描述简洁,我们会大量使用英文缩写的DDD来表示领域驱动设计这一概念和名称。
本节将从几个常见问题出发,讨论DDD的基础知识。
关于DDD,我们首先需要回答如下3个问题。
(1)什么是DDD?
总体来说,DDD提供的是一套开展业务建模和软件设计的方法论。和其他方法论的不同之处在于,DDD强调开发人员与业务领域专家高效协作,从而共同交付业务价值。从架构设计上说,DDD认为良好的系统架构应该是技术架构和业务领域相互融合的结果,不能脱离业务领域设计技术架构。需要注意的是,DDD不是设计准则或者规范,也不是架构设计的“脚手架”。事实上,关于如何实现DDD,业界并没有给出统一的标准,这也是需要系统化学习DDD的原因。
(2)为什么需要DDD?
学习DDD的根本原因在于软件复杂度,关于软件复杂度的表现形式和关注点,请参考1.1节。读者可以充分扩散思维,想象一下日常开发中的痛点。针对软件复杂度,我们的思路是清晰划分软件的关注点,并通过拆分和建模在一定程度上降低系统复杂性。而DDD正是为了降低软件复杂度而诞生的软件设计方法。
(3)为什么DDD难学?
很多读者反馈DDD非常难掌握。实际上这并不代表DDD本身的学习难度也是如此,困难更多体现在学习方法和思维上。图1-19展示了DDD学习的表象与真相。
图1-19 DDD学习的表象和真相
诚然,DDD是一种比较复杂的建模方式,其中包含一系列相对晦涩难懂的核心概念,包括限界上下文、聚合、实体、值对象等。但这些概念并不是虚无缥缈的,而是依托于严谨的逻辑分析思维,包括拆分、边界、交互、集成和架构等。普通开发人员之所以觉得DDD概念多、不易理解,正是因为缺少这些逻辑分析思维。而DDD的创始人Eric Vans之所以能够提出DDD,也是因为他自身就是一名优秀的架构师,具备强大的逻辑分析思维能力,能够将这一能力与业务抽象、系统建模整合起来。原则上,如果读者拥有足够丰富的逻辑分析思维,也可以创建一种系统建模方法论并做到自圆其说。
领域(Domain)本质上是对现实世界问题的一种统称,是一种业务开展的方式,用以体现一个组织所做的事情,以及其中所包含的一切业务范围和所进行的活动。例如,电商系统包含商品、订单、库存和物流等业务概念,而医疗健康系统则关注挂号、就诊、用药、健康报告等业务场景。领域概念的提出不仅从业务的角度体现了系统的功能和价值,而且从技术的角度为人们提供了设计思想。
我们以一个业务场景为例进行讨论。试想一下日常生活中的生病就医场景。为了完成一次就医过程,用户需要完成预约挂号、向医生述说身体症状、做各种检查并获取报告、根据检查结果进行用药等步骤。如果将这些步骤抽象为一个问题空间(Problem Space),那就是就诊。如果我们设计一个针对这一场景的系统,所有的环节都是为了更好地帮助用户就诊,这是对真实世界的一种表现。
那么,如何针对就诊这个问题空间提供对应的解决方案呢?这就需要引入解空间(Solution Space)的概念。解空间代表的是一种逻辑世界,通过设计语言和设计模型来解决真实世界中的问题。图1-20展示了问题空间和解空间之间的映射关系。
图1-20 问题空间和解空间的映射关系
那么问题又来了,我们应该如何设计解空间呢?这需要对系统进行建模,从而得到能够指导系统开发的业务模型(Business Model)。系统建模是一个复杂的话题,围绕这一话题,业界也形成了不同的建模方法,而DDD同样在系统建模领域占有重要地位。接下来,我们来看一下在DDD中业务模型的组成结构,图1-21展示了从解空间到业务模型的表现形式。
图1-21 从解空间到业务模型的表现形式
在图1-21中,我们通过7个问题对业务模型进行了分析。接下来,我们将围绕这7个问题展开介绍。
● 业务描述。业务模型需要通过简洁而通用的语言进行描述,从而确保与模型相关的所有人都能够对模型所代表的业务场景和需求达成统一认识。
● 业务拆分。业务场景的复杂度决定了业务模型中功能组件的数量和关联关系,我们需要通过拆分的方式明确各个功能组件之间的边界。
● 业务对象。在一个业务场景中,势必存在一组业务对象,这些业务对象通过一定的交互关系构成具体的业务场景。
● 业务规则。在一个业务模型中,内部的核心逻辑通过一系列的业务规则来进行展现,业务规则代表着具体领域下的业务价值。
● 业务状态。每个业务场景都拥有状态,这些状态构成了业务处理的流程和顺序,也是业务建模的重点对象。
● 业务数据。所有业务模型都会产生数据,而且业务规则和业务状态的设计很大程度上围绕业务数据的处理过程而展开,我们需要将核心业务数据持久化保存。
● 业务外观。对于一个业务模型,需要和客户端、其他业务模块及第三方外部系统进行集成。这就需要开放一定的交互入口,我们将这部分入口称为业务外观(Facade)。
DDD针对业务模型的以上7个问题给出了对应的设计方法。在此之前,我们先对DDD的设计维度进行分析。
DDD有两个主要的设计维度——战略设计和战术设计。
● 战略设计。战略设计关注如何设计领域模型,以及如何对领域模型进行划分,其目的在于清楚界分不同的系统与业务关注点。战略设计是一个面向业务、具备较高层次的设计维度,侧重于业务领域的梳理,以及考虑如何将业务领域和技术架构整合的问题。
● 战术设计。战术设计关注技术实现,从技术的层面指导开发人员实施领域驱动设计,关注在领域模型的基础上采用特定的技术工具来开发系统。显然,战术设计体现了技术架构的设计和展现方式。
战略设计和战术设计的整合为开发人员提供了一套通用的建模语言和术语,并展示了基于领域驱动的架构设计方法和实现DDD的各项关键技术,如图1-22所示。
图1-22 DDD中的战略设计和战术设计
接下来我们将结合业务模型及DDD的两大维度展开讨论。
我们先来看领域驱动的战略设计。战略设计包含统一语言和限界上下文这两个核心概念,它们与业务模型的对应关系如图1-23所示。
图1-23 业务模型与战略设计的对应关系
通俗地说,统一语言(Ubiquitous Language,也称作通用语言)就是团队成员的“行话”,面向的是业务而不是技术。在协作过程中,业务人员和技术人员在意识形态与认知体系上达成一致并不是一件容易的事情,一方面需要领域专家持续介入,另一方面需要开发者具备对业务领域的基本思考方法。
统一语言的建立通常并不是一步到位的,而是分层次持续演进的。例如,考虑一个用户健康监控和管理的业务场景,业务人员和开发人员经过初步沟通,得到如下统一语言。
构建统一的健康监控功能,用户可以通过这一功能管理自己的健康信息。
在上述场景下,对原始需求的描述构成了系统的最高层次的统一语言,后续从业务到技术的各个层次的统一语言都将由此展开。而在开发人员与业务人员进一步沟通之后,得到如下细化的统一语言。
用户在申请健康检测时会生成一个健康检测单,同一个用户在上一个健康检测单没有完成之前无法申请新的检测单。
用户在申请健康检测单时提供自己的既往病史及目前的症状描述,然后系统根据用户的这些输入信息生成一个健康计划,健康计划被看作管理用户健康数据的一种执行媒介。
一个用户在同一时间只能有一份生效的健康计划,如果用户对系统自动生成的健康计划并不满意,可以重新申请生成健康计划。
健康计划的具体内容包括计划的制定医生、计划的描述、执行的周期、需要用户执行的健康任务列表等数据。
健康检测的结果表现为一种可以量化的健康分,该健康分会根据用户执行健康任务的完成情况不断更新。用户可以通过健康分判断自己的健康状况。
上述对业务场景的描述构成了第二层级的统一语言,我们可以从这些描述中提取大量有助于开展系统设计工作的关键信息。
统一语言的表现形式可以多样化,常见的有术语表、文档和图、模型语言等。在第3章中,我们将详细介绍统一语言的表现形式。
针对业务拆分,我们首先需要引入子域(Sub Domain)的概念。我们可以将领域拆分为多个独立的子域。子域作为系统拆分的切入点,其来源往往取决于系统的特征和拆分的需求,如这些需求属于核心功能、辅助性功能还是第三方功能等。
拆分子域之后,我们需要进一步明确限界上下文(Bounded Context)的概念。限界上下文是DDD中的核心概念,这个概念比较难以理解,我们可以把这个词拆开来进行解释,即限界上下文=限界+上下文。
● 限界。对于任何概念、属性和操作,每个领域模型在特定的业务边界之内具有特定的含义,这些含义只限于这个边界之内,同一个业务概念,在不同的限界上下文中代表着不同的领域模型,这就是“限界”这个名称的由来。
● 上下文。上下文用来表现业务流程的场景片段。随着业务的开展,上下文会因为某些活动的发生而形成场景的边界。
我们来看一个限界上下文的简单示例,如图1-24所示。
图1-24 限界上下文示例
图1-24描述的是一个客服管理系统的业务场景,用户基于某一个订单(Order)发起一个客服工单(Ticket),该工单会交由某一个客服人员(Staff)进行处理。这里出现了3个限界上下文,其中Ticket上下文和Staff上下文中都存在一个Staff对象。两个Staff对象虽然名称相同,也代表着同一个逻辑概念,但在业务建模过程中却有本质性的区别。随着内容的演进,会发现Staff上下文中的Staff是一个聚合(Aggregate)对象,而Ticket上下文中的Staff则是一个实体(Entity)对象。另外,Ticket上下文中的Ticket对象可能依赖Order上下文中的Order对象,但Ticket和Order显然属于不同的业务场景,此时可以发现,通过划分限界可以在很大程度上影响系统的设计和实现。
有了子域和限界上下文,下一步就是将它们整合到一起。每个子域都有自己的限界上下文,可以根据需要有效整合各个限界上下文,从而构成一个完整的领域模型。
请注意,在DDD的战略设计中,统一语言属于问题空间的范畴,而限界上下文属于解空间的范畴。也就是说,通过战略设计,我们已经完成了从问题空间到解空间的映射。但是,这时的解空间只是一个框架,需要通过DDD的战术设计进行填充。DDD战术设计与业务模型的对应关系如图1-25所示。
图1-25 业务模型与战术设计的对应关系
在传统软件开发中,业务是由数据驱动的,开发人员从数据的角度来规划对象的组织形式,并以面向数据库的方式对这些数据对象进行设计和建模,每个业务对象只包含业务数据和结构的定义,并不具备业务操作能力,这就是所谓的贫血模型(Anaemic Model)。
虽然以数据作为主要关注点的开发模式也能完成对系统的构建,但我们认为面向领域的模型对象才是能够表达统一语言的有效载体。究其原因,在于很多对象并不能简单地通过它们的数据属性来定义,而是应该具有一系列的标识和行为定义。在DDD中,领域模型对象包括三大类,如图1-26所示。
图1-26 领域模型对象的三大类
在图1-26中,有聚合、实体和值对象3类领域对象,我们认为这些领域对象才是能够表达统一语言的有效载体。
● 聚合。聚合的核心思想在于简化对象之间的关联关系,一个聚合内部的所有对象只能通过聚合对象来进行访问,从而有效降低了对象之间的交互复杂度。
● 实体。实体是聚合内部具有唯一标识的一种业务对象,具有丰富的操作行为、状态可变性,以及完整生命周期。
● 值对象。值对象有点类似贫血模型对象,只关心对象的数据属性而不具备操作行为。值对象是不变对象、没有唯一标识且通常不包含业务逻辑。
我们可以将业务模型中的业务逻辑抽象为一组业务规则。业务规则从概念上说通常不属于任何一个独立的对象,而是涉及一组领域模型对象之间的交互和操作。当领域模型中某个重要操作无法由单个聚合或实体完成时,应该为模型添加一个独立的访问入口,这就是领域服务(Domain Service),如图1-27所示。
图1-27 领域服务的定位
从图1-27可以看出,领域服务的构建涉及多个领域模型对象之间的交互和协作,这是单个领域模型对象所不能完成的操作。
现实中很多场景可以抽象为事件,如某一个操作发生时发送一条消息、出现了某种情况执行某个既定业务操作等。本质上,事件代表的是一种业务状态的变化,是一种独立的建模对象,在DDD中被称为领域事件(Domain Event),如图1-28所示。
图1-28 领域事件的执行流程
领域事件实质上就是将领域中发生的活动建模为一系列离散事件。领域事件也是一种领域对象,是领域模型的重要组成部分。
对于任何一个系统,业务数据都需要进行统一的管理和维护,开发人员应将数据保存到各种数据持久化媒介中。我们认为系统中应该存在一个专门针对数据访问的入口,通过该入口可以对所有领域模型对象进行遍历。在DDD中,资源库(Repository)实际上充当了领域模型对象提供者的角色,如图1-29所示。
图1-29 资源库的定位
简单来说,资源库用于实现对业务数据的持久化管理,同时帮助开发人员屏蔽数据访问过程中的技术复杂性。
DDD中的应用服务(Application Service)提供了类似数据传输对象(Data Transfer Object,DTO)模式和外观模式(Facade Pattern)的功能。我们希望为系统中的一组接口提供一个一致的界面,从而使其更易用,这就是应用服务的价值,如图1-30所示。
图1-30 应用服务的定位
对于应用服务,我们不应该放置任何与业务逻辑相关的操作,而是仅完成来自用户界面或外部系统的集成需求,所以是很薄的一层技术组件。
到此,我们完成了从业务模型到DDD方法的完整映射,读者可以结合图1-31中的内容进行总结和回顾。
图1-31 业务模型与DDD之间的映射关系
请注意,图1-31中DDD的各个组件并不是位于同一层次的,各个限界上下文都应该包括战术设计的所有技术组件,如图1-32所示。
图1-32 限界上下文和战术设计技术组件示意图
我们引入DDD的目标是控制软件复杂度。那么,DDD是如何做到这一点的呢?本节将探讨DDD与软件复杂度之间的对应关系。
我们先来看一个示例。图1-33展示了一张UML中的用例图,其中涉及工单系统中的一个常见用例。
图1-33 工单系统常见用例
针对图1-33,我们可以采用DDD中的设计方法进行建模和拆分,从而得出一组子域,如图1-34所示。
图1-34 工单系统子域
我们可以将图1-34中的每个子域映射为限界上下文,并梳理限界上下文之间的关联关系,如图1-35所示。
图1-35 工单系统中限界上下文的交互过程
在图1-35中,U代表上游,D代表下游。位于下游的Ticket上下文会分别调用位于上游的其他3个限界上下文。通过这种方式,我们对系统进行了合理的拆分,从而降低了因规模导致的软件复杂度。这也是DDD中应对这一软件复杂度问题的基本手段。
在DDD中,为了应对由于结构导致的软件复杂度,通常会引入一组架构模式。图1-36展示的就是最基础的DDD分层架构模式。
可以看到,这里将系统分为用户接口层、应用层、领域层及基础设施层4个层次,并梳理了各个层次之间的调用关系。这是一种常见的DDD架构模式,但并不是DDD中唯一可以采用的架构模式,我们也可以采用图1-37所示的六边形架构来对系统结构进行合理组织。
图1-36 DDD分层架构模式
图1-37 DDD六边形架构模式
在DDD中,常见的架构模式还包括五层分层架构、整洁架构等。关于分层架构、六边形架构及其他DDD架构模式的讨论,请参考第11章。需要牢记的是,无论采用何种架构模式,目的都是降低由于结构不合理而导致的软件复杂度。
应对变化所引起的软件复杂度的基本思路是抽象,那么DDD是如何实现抽象过程的呢?本质上是依靠它的领域对象。我们在1.2节讨论DDD战术设计维度时已经知道DDD领域对象包括聚合、实体和值对象3种类型,它们之间存在图1-38所示的包含关系。
图1-38 3种领域模型对象的结构包含关系
可以看到,聚合内部包含实体和值对象,而实体和值对象既可以单独存在,也可以在实体中嵌入值对象。实体具有操作行为和状态可变性,而值对象则只关注数据属性。这两种领域对象很好地封装了业务本身的属性和状态。另外,聚合的作用在于管理领域对象之间的交互过程,一个聚合内部的所有对象只能通过聚合根进行访问,从而有效降低了对象之间的交互复杂度,如图1-39所示。
图1-39 通过聚合根降低对象交互复杂度
本质上,聚合、实体和值对象体现的是一种抽象机制,抽象的切入点包含数据属性、操作行为、可变状态及交互方式。通过前面内容的分析,我们可以基于DDD来应对系统中的各种变化,并通过抽象手段降低软件复杂度。
在1.2节介绍DDD的基本概念时,我们提到DDD不是设计准则或者规范,也不是架构设计的“脚手架”。因此,当在系统开发中使用DDD时,势必需要与目前业界主流的架构体系进行融合,这是DDD得以落地的核心。本节将围绕这一话题展开讨论。
很多读者可能会问:在开发系统时使用的是单体架构,而在单体应用中是否可以使用DDD呢?当然可以。在单体应用中,组织业务逻辑时,仍然推荐使用DDD的设计思想和方法。
所谓单体系统,简单来说,就是将一个系统所涉及的各个组件打包为一个一体化结构并进行部署和运行。在Java领域中,这种一体化结构很多时候表现为一个JAR包,而部署和运行的环境就是以Tomcat为代表的各种应用服务器。这就意味着在每个单体系统中,我们至少可以实现一个包含所有领域模型、领域服务、领域事件、资源库和应用服务的限界上下文,如图1-40所示。
图1-40 单体系统中的单个限界上下文
在图1-40的基础上,我们可以采用业务模块(Module)的方式对单体系统进行拆分,这样得到的一个个模块就是限界上下文。图1-41展示了这种拆分效果。
图1-41体现了一种逻辑拆分的设计思想,即所有的代码虽然仍位于同一个JVM物理进程中,但逻辑上这些代码是严格按照DDD的方式进行拆分后的产物。基于这种方式,如果需要将单体系统升级为微服务架构,只需将各个业务模块提取为独立的微服务。
图1-41 单体系统中基于模块拆分限界上下文
近年来,微服务架构的持续发展也为DDD的应用带来了一波高潮,这是因为DDD中的限界上下文概念和微服务架构中的服务之间存在非常契合的对应关系。如果正在实施微服务架构,那么只需要将每个限界上下文映射成微服务即可,如图1-42所示。
图1-42 微服务系统基于限界上下文拆分服务
有些读者可能会问:限界上下文和微服务之间是一对一的映射关系吗?答案是否定的。实际上,一个限界上下文中可以包含多个微服务,也就是限界上下文和微服务是一对多的关联关系。图1-43展示的就是现实场景中的一个示例,可以看到3个限界上下文中一共包含6个微服务。
图1-43 限界上下文和微服务的一对多关系
图1-44进一步展示了限界上下文和微服务在范围上的差异性,读者可以通过该图来加深对其的理解。
图1-44 限界上下文和微服务的范围差异
中台和微服务并不是同一层面的事物,可以简单认为微服务是构建中台的一种组件化实现手段。中台通常分为业务中台和数据中台两种类型。
在业务中台架构中,每个中台都由一组微服务构成。因此,我们可以在微服务架构的基础上添加对中台架构的描述,如图1-45所示。
图1-45 业务中台和DDD
对于数据中台,我们也可以将整个围绕业务数据处理的后台逻辑及分析模型划分到限界上下文中,从而针对不同的数据处理过程提取不同的限界上下文。图1-46展示了这一建模方法。
图1-46 数据中台和DDD
和其他任何一种软件架构设计方法一样,DDD同样适用于所有业务系统的开发。在采用DDD之前,我们需要结合自身正在开发的业务系统,完成DDD和所采用架构之间的融合。
本章首先系统阐述了软件复杂度的3种表现形式——规模、结构和变化,并由此引入DDD的基本概念,以及战略设计和战术设计这两大类设计体系。使用战略设计和战术设计,我们可以分别应对软件复杂度的不同表现形式。另外,本章还讨论了作为一种主流的系统建模方法,DDD与单体应用、微服务架构及中台架构之间的融合方法。