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