写在前面:
今天我接着跟大家聊一聊,DDD概念复杂难懂,实际落地如何设计代码实现模型。或许你是刚看到关于这部分的内容,想着这里我有必要多说一句,关于这个话题,框架上,分为这样两部分讲的:方法篇 + 实践篇。
前一部分,方法篇。旨在详细介绍DDD所包含的几个核心概念,以及围绕这些概念所构建的DDD代码实现模型的组成结构。至于为何有必要讲,上一篇开头我有明确告诉大家。
另外,考虑到有的朋友可能才刚点进来,还没看过上一篇,或者没来得及看,故而这里也再点明说一下 我想分享这一话题的必要性,以便于帮你快速知晓可以或多或少有哪方面的收获。
开门见山说,可惜的是,目前业界关于如何实施这些概念,并没有一套统一的标准和规范,这就导致我们在具体的开发过程中,常常感到无从下手。
为此,本文专门提炼了一整套DDD代码实现模型。
此外,关于看的方式,我多说一句。基于是分为前后两部分更新发布的,这就涉及到先后了。若上一篇你还未看,朋友,建议你可以先花上几分钟,或者结合文章中大小标题的思路引导,大致了解下行文框架。咱们可以过完上一篇,再进入这篇的分享,结合着,效果更好。
01 如何设计DDD代码实现模型?
在分析DDD代码实现模型时,对于上一篇提到的四个组成部分,我们需要梳理它们的代码结构和依赖关系。针对代码结构,我们需要明确代码包的组成,以及内部所包含的技术组件。
在明确了包结构之后,依赖关系指的是我们需要进一步明确这些代码包和技术组件之间的交互关系。基于这两点,让我们先来讨论领域对象的代码实现模型。
▶︎ 领域对象代码实现模型
针对领域对象,我们通常用“domain”这个单词,对代码包结构的顶层包进行命名,在该包结构下的所有技术组件,都属于领域对象的范畴。
具体而言,在DDD中,领域对象包括领域模型对象、领域事件、资源库以及应用服务所涉及到的命令和查询对象,其中领域模型对象可以分为聚合、实体和值对象这三大类。
因此,在DDD所有的代码实现模型中,领域对象涉及的代码结构最为复杂,可以分成两个层次,如图1所示。
图1
可以看到,这里的“domain”代表整个领域对象,而“model”则代表领域模型对象,请注意这两者在命名上的区别,以及它们之间的从属关系。领域对象是DDD代码实现模型的基础,包含核心业务逻辑的实现。
▶︎ 应用服务代码实现模型
类似地,针对应用服务,我们通常使用“application”来命名顶层包结构。应用服务包含查询服务和命令服务这两大类,所以在子包的命名上,也会用“commandservice”和“queryservice”加以区分,如图2所示。
图2
显然,命令服务和查询服务,分别依赖于领域对象代码实现模型中的命令对象和查询对象,我们用虚线表示这层依赖关系。在DDD的代码实现模型中,应用服务可以说是交互关系最为复杂的一个代码模型。
一方面,它需要将命令和查询操作,分派给聚合对象等领域模型对象。
另一方面,它也需要分别和基础设施,以及其他限界上下文进行交互。
关于后者,我们在讨论到案例分析时,还会做进一步展开。
▶︎ 基础设施代码实现模型
其实,所谓的基础设施,指的是DDD应用程序中所使用到的各种具体技术、工具和框架。常见的基础设施类组件主要包括这几个方面:
- 数据持久化(Persistence)
- 消息通信(Messaging)
- 系统配置(Config)
- 安全控制(Security)
因此,基础设施的包结构并不是固定的,而是根据具体的技术开发要求进行灵活的组织,这里给出一个常见的包结构,如图3所示。针对基础设施,我们使用了“infrastructure”,对这一包结构进行命名。
图3
上图中有一点需要注意,代表数据持久化的“persistence”包,和代表消息通信的“messaging”包,在基础设施代码实现模型中是最常见的,因为它们分别对应着领域对象中的资源库和领域事件。
在DDD中,资源库和领域事件的定义位于领域对象代码实现模型中,它们与具体的实现技术无关。而与具体实现技术相关的持久化和消息通信,则位于基础设施代码实现模型中。这里体现了领域对象与实现技术相互分离的设计原则。
▶︎ 上下文集成代码实现模型
最后,我们来讨论上下文集成代码实现模型。需要注意的是,这个模型实现起来难度最大,因为涉及到多种系统集成技术体系。
针对这一代码实现模型,我们首先需要明确它是面向多个限界上下文的,所以我们需要考虑数据的流向,也就是所谓的内向(Inbound)数据和外向(Outbound)数据。
一方面,限界上下文,需要暴露访问入口供其他上下文进行使用。站在当前上下文角度看,这是一个Inbound操作。而当某一个上下文向外部上下文发起请求时,这就是一个Outbound操作,如图4所示。
图4
在代码实现模型的设计上,我们也将采用“inbound”和“outbound”来命名包结构。那么这两个包结构下,应该包含哪些技术组件呢?
我们先来讨论“outbound”包结构,如图5所示。 图中,“rest”包中的REST API将外部请求,转化为内部的Command和Query对象,并交由应用服务进行处理。在这个转化过程中,通常需要引入专门的DTO(Data Transfer Object,数据传输对象)对象,和组装器(Assembler)对象。
图5
同时,“eventpublisher”包中的事件发布器(Event Publisher),则用来面向外部限界上下文发布领域事件。
接着,我们讨论“inbound”包结构。在一个限界上下文中,数据的Inbound操作主要有两类,一类是防腐层(Anti-Corruption Layer,ACL),用来向远程REST API发起请求并获取结果。另一类是用来完成对领域事件进行响应的事件处理器(Event Handler),如图6所示。
图6
基于上下文集成过程,两个上下文中的“inbound”和“outbound”包结构中所包含的技术组件,实际上是一一对应的,如图7所示。
可以看到,一个限界上下文“inbound”中的“acl”和“eventhandler”,分别对应着另一个限界上下文“outbound”中的“rest”和“eventpublisher”。
图7
至此,关于DDD中四大类代码实现模型,已介绍完。在接下来的内容中,我们将基于一个具体的应用场景,通过案例分析,将这些代码实现模型付诸于实践。基于这个案例,你可以将本文前面介绍的所有内容,和日常开发过程联系起来,进一步掌握将模型转化为具体代码的实现方法和技巧。
02 DDD代码实现模型案例分析
在现实世界中,工单处理是一个非常常见的业务需求。而工单的发起,通常都是因为用户需要对订单进行咨询或投诉。
在这个场景中,基于DDD的设计方法,我们可以分别拆分出工单(Ticket)、客服(Staff),以及订单(Order)这三个限界上下文。在这三个上下文中,Ticket上下文,会分别与Staff和Order这两个上下文进行集成,从而创建工单申请,如图8所示。
请注意,图中展示了Ticket上下文,所具备的两种不同的上下文集成方式。
针对Staff上下文,Ticket上下文将使用REST API,完成对工单中客服数据的获取。
而针对Order上下文,则使用了领域事件,即一旦Order的状态发生变化,Order上下文会发送对应的领域事件到Ticket上下文中。
图8
▶︎ Ticket上下文代码实现模型示例
显然,针对这一场景,Ticket上下文同时具备了Inbound和Outbound操作。因此,它的代码实现模型是最完整的,如图9所示。
图9
上图中,我们使用IDEA这款开发工具和Spring Boot这一特定的开发框架,构建了Ticket限界上下文的代码实现模型。我们可以很清晰地看到,DDD四种代码实现模型的表现形式,就是五个顶层的代码包结构。其中,上下文集成代码实现模型同时包含了“inbound”和“outbound”这两个代码包。
我们再对这些顶层代码包结构做展开,可以得到如图10所示的子代码包结构。
图10(上下滑动查看)
上图所示的所有子代码包结构,在前面的内容中也都已经给出了相应的描述,这里便不再赘述。
Ticket上下文中,命令服务TicketCommandService完成了对Staff服务的上下文集成,这时候采用的是防腐层ACL组件,示例代码如下所示。
可以看到,这里使用AclStaffService这个ACL组件,对Staff服务发起了远程调用,然后把返回结果填充到命令对象,并创建Ticket聚合。最终,我们通过TicketRepository完成了对聚合对象的持久化操作。
图11
上述AclStaffService,就完成了对Staff上下文所提供的REST API的调用,示例代码如下所示。这里用到了Spring自带的RestTemplate模板工具类,完成对远程HTTP端点的访问操作。
图12
▶︎ Staff上下文代码实现模型示例
在Staff上下文,我们需要完成对上述REST API的构建,它的代码工程结构如下图所示。
可以看到,相较Ticket上下文,Staff上下文的代码结构比较简单,因为该上下文只需要提供对外的“outbound”包,而基础设施部分也只需要完成对领域对象的持久化操作即可。
图13
▶︎ Order上下文代码实现模型示例
最后,我们来到Order限界上下文,它的代码实现模型是这样的,可以一同看下。
图14
我们知道Order上下文,提供了针对Order数据的领域事件发布机制,所以它的“outbound”包中包含了用于发布领域事件的“eventpublisher”子包,并提供了一个OrderEventPublisherService,如下所示。
图15
这里通过Spring Cloud Stream,实现了领域事件的发布。而在Ticket上下文中,我们同样可以基于Spring Cloud Stream,实现对该领域事件的监听和消费,示例代码如下所示。
图16
请注意,上述OrderUpdatedEventHandler,位于Ticket上下文“inbound”包的”eventhandler”子包中。
关于这些具体实现代码的讲解不是本文的重点,你可以参考笔者在Github上的案例代码进行系统学习:https://github.com/tianminzheng/customer-service。
03 总结和延伸思考
今天的分享到这里就结束了。本文内容详细回答了开发人员,在实现DDD应用程序中所碰到的一个核心问题,即如何构建DDD的代码实现模型。之所以要讨论这个话题,原因在于DDD中的很多概念都比较晦涩难懂,而业界也没有为如何实现这些概念,提供统一的开发规范和标准。
而通过将DDD中的各种复杂概念与具体代码实现模型进行映射,在帮我们更好地理解这些概念的同时,也能够将它们直接应用到日常开发过程中。
通过本文内容的介绍,开发人员可以结合自身的业务开发需求,设计一套完整的DDD代码实现模型。这里也附上全文思维导图,助你回顾、梳理思路等。
图17 全文思维框架导图-帮助你快速回顾、梳理、总结
▶︎ 最后,我觉得还是有必要强调一点
本文中给出的DDD代码实现模型,也只是一个参考模型。而代码实现模型的设计,也与具体所采用的技术体系有一定关联。在本文所展示的案例中,我们使用了Spring Boot、Spring Cloud Stream等Spring家族中的开发框架,来开发DDD应用程序。
而如果你使用Axon这种基于事件溯源模式的DDD开发框架,那么在代码实现模型中,就需要引入用于事件分发和存储的Gateway、EventStore等组件,而位于基础设施中的传统数据持久化组件,可能就不一定会被使用到。
当然,基于我们今天介绍的内容,相信你并不难对这套DDD代码实现模型进行扩展。DDD作为一种系统建模方法论,也存在一些诸如分层架构、整洁架构、六边形架构等多种架构风格。
针对每种架构风格,我们都需要设计对应的代码实现模型。
而基于本文中介绍的内容,通过对DDD中各个核心概念与实现模型之间进行合理的映射,我在文中提供了一套设计代码实现模型的系统方法,从而帮助你可以应对不同架构风格的实现要求。
这也是本文的核心价值所在。