今天我想与你聊一聊,DDD概念复杂、难懂,实际落地该怎么设计代码实现模型。关于这个话题,先说说整体框架、思路,我打算结合两部分分享给你,每一部分,相信仔细看完,都会或多或少有所收获。以下内容,预计1分钟左右可快速看完:
前一部分,方法篇,旨在详细介绍DDD所包含的几个核心概念,以及围绕这些概念所构建的DDD代码实现模型的组成结构。
后半部分,实践篇,进一步思考。我继续接着说,承接前面的内容,要想让这些代码实现模型真正落地,我们需要把它们与具体的应用场景结合起来。我将侧重详细阐述DDD代码实现模型的设计方法,并给出一个具体的案例分析。
伴随着业务系统复杂度的不断提升,以及微服务架构等分布式技术体系的大行其道,领域驱动设计(Domain Driven Design,DDD),日渐成为系统建模领域的主流设计思想和模式。在DDD中,引入了限界上下文、聚合、实体、值对象、领域事件、资源库、应用服务等一系列核心概念。
通过这些概念,开发人员可以开展系统设计和实现工作。但是,DDD中的这些概念相对都比较抽象,甚至有些晦涩难懂。再往相通或类似问题点上靠,我认为实质上对于复杂难懂的概念的理解和把握,我们一开始不必过于纠结这些概念本身,而是可以把它们与现实中的具体实现模型对应起来。
通过两者之间的合理映射,来促进对概念本身的理解,如下图所示。这里多说一句,即便你是其他技术领域的朋友,或许也曾遇到过类似问题,并有着共通性。希望看完今天的分享,可以或多或少帮助到你,并有所启发、思考。
在上图中,我们一方面尝试把复杂概念映射到实现模型。另一方面,基于对实现模型的把握,可以反推对复杂概念的理解程度,从而更好地掌握这些概念。这也更足以见得实践才能出真知,也只有设计过实现模型,才能真正掌握这些概念,从而把它们应用到各种具体的场景中。
这是一种行之有效的办法。
那么问题就来了,在日常开发过程中,如何确保DDD真正落地,把这些抽象概念转为具体代码模式,是我们今天要讨论的内容。
01⎪
想设计代码实现模型,咱非得了解DDD中这几个核心概念?
总体来说,DDD提供的是一种开展业务建模和软件设计的方法论。DDD认为良好的系统架构,应该是技术架构和业务架构相互融合的结果,开发人员不能脱离业务领域来设计技术架构。为了实现这一目标,DDD提出了一组核心概念,如图1所示。
我们先来看第一个核心概念,就比较难于理解,即限界上下文(Boundary Context)。在DDD中,当我们把业务领域拆分成多个子域之后,限界上下文明确了子域的业务界限,并实现子域与子域之间的隔离,如图2所示。
有了限界上下文,我们就需要围绕业务场景设计领域模型对象(Domain Model Object)。领域模型对象,包含了丰富的业务逻辑和操作行为,这点和只包含数据属性的传统数据对象,有本质区别。
因此,领域模型对象是我们在应用DDD时,最应该关注的一组对象,也是最难把握的一组对象。
在DDD中,领域模型对象包括三大类,即聚合(Aggregate)、实体(Entity)和值对象(Value Object),这三类对象各有特点。
相较领域模型对象,领域事件诞生较晚,但也是领域模型的一个重要组成部分,因为现实中很多场景,都可以抽象成事件(Event),如图4。
在DDD中,通过领域事件可以实现业务状态变化的有效传播,并在单个限界上下文内部或在多个上下文之间,对这些状态变化做出响应。
业务领域中的各种状态变化最终都需要进行存储。为此,DDD提供了一个针对业务数据的统一访问入口,这就是资源库(Repository)。通过资源库,我们可以实现对各种领域对象的持久化操作,如图5所示。
最后,我们来引入应用服务的概念。应用服务包括命令(Command)服务和查询(Query)服务两大类,本质上起到的是一种解耦和协调作用,确保各种领域模型对象之间的交互和协作。因此,在涉及到多个限界上下文之间的交互时,我们需要重点关注应用服务。如图6所示。
02⎪
概念复杂又难懂,想实际业务场景下真正落地,需引入DDD代码实现模型
关于DDD中的核心概念,我就简单介绍到这里,下一步就是要讨论一个所有开发人员都必须面对的话题,即如何将这些复杂难懂的概念,在现实的开发过程中能够真正落地?这就需要引入DDD代码实现模型。
▶︎ 要想设计代码实现模型,先得搞清楚它有哪几部分组成?
无论设计方法有多好,能够转换为可运行的代码才是王道,这点对于DDD而言尤为如此。
可惜的是,目前业界关于如何实施这些概念,并没有一套统一的标准和规范,这就导致我们在具体的开发过程中,常常感到无从下手。为此,本文专门提炼了一整套DDD代码实现模型。接下来,让我们从DDD代码实现模型的基本概念和组织结构展开讨论。
▶︎ 在讲代码实现模型之前,先弄清楚什么是实现模型
说起模型(Model),业界主流的方法论认为存在三种不同的类型,即领域模型、设计模型和代码模型,如图7所示。
关于领域模型,我们在前面的内容中已经做了介绍。在DDD中,聚合、实体、值对象、领域事件等,都可以归属到这一模型的范畴。
而设计模型(Design Model),可以分成边界模型和内部模型两个组成部分。边界模型明确系统边界,抽象系统集成和交互方案。而内部模型细化边界模型,在明确系统边界的前提下,实现系统内部模块和组件的抽象和构建。因此,在DDD中,我们往往从限界上下文的角度出发,来开展设计模型的建设,如下图所示。
最后,代码模型为现实世界的解决方案,提供可执行的系统环境。我们可以通过在领域模型和设计模型中嵌入代码的方式来构建代码模型,该模型是将DDD各个复杂概念转换为可执行代码的关键所在,也是我们今天要讨论的主要内容。
显然,领域模型、设计模型和代码模型之间,存在一种层次依赖关系,如图9所示。
首先,领域模型代表领域的固有业务;
设计模型指向领域模型,关注对外部接口的承诺以及交互关系;
代码模型提供了完整实现过程,是对设计模型的细化。
正是通过这三种模型的整合,完成了从现实问题到最终能够落地的实现方案的演进。
▶︎ DDD代码实现模型,应包含哪些部分?
针对DDD代码实现模型的讨论,我们也将遵循上述三种模型的整合过程。结合DDD中的各种核心概念,我们梳理DDD代码实现模型组成结构,如图10所示。
在上图中,我们可以清晰看到DDD代码实现模型的四个组成部分,分别面向领域对象、应用服务、基础设施以及上下文集成。讲到这里,你可能会问,为什么我们要这样设计DDD的代码实现模型呢?
我们知道一个完整的DDD应用程序,通常由多个限界上下文构成。因此,对于代码实现模型而言,我们需要重点考虑两个维度,即:
- 单个限界上下文实现过程中的代码模型
- 多个限界上下文之间集成过程中的代码模型
在上图中,关于领域对象、应用服务、基础设施代码实现模型的讨论,属于单个限界上下文的范畴,而上下文实现代码集成模型,显然面向多个限界上下文,如图11所示。
通过前面内容的学习,相信你对DDD代码实现模型的组成结构,已了然在胸。
那么,在日常开发过程中,我们应该如何设计这些代码实现模型呢?有没有具体的案例可以参考呢?这几个问题点,你可以先停下来琢磨下。
03⎪ 如何设计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中四大类代码实现模型,已介绍完。在接下来的内容中,我们将基于一个具体的应用场景,通过案例分析,将这些代码实现模型付诸于实践。基于这个案例,你可以将本文前面介绍的所有内容,和日常开发过程联系起来,进一步掌握将模型转化为具体代码的实现方法和技巧。
04⎪ 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。
05⎪ 总结和延伸思考
今天的分享到这里就结束了。本文内容详细回答了开发人员,在实现DDD应用程序中所碰到的一个核心问题,即如何构建DDD的代码实现模型。之所以要讨论这个话题,原因在于DDD中的很多概念都比较晦涩难懂,而业界也没有为如何实现这些概念,提供统一的开发规范和标准。
而通过将DDD中的各种复杂概念与具体代码实现模型进行映射,在帮我们更好地理解这些概念的同时,也能够将它们直接应用到日常开发过程中。
通过本文内容的介绍,开发人员可以结合自身的业务开发需求,设计一套完整的DDD代码实现模型。这里也附上全文思维导图,助你回顾、梳理思路等。
图17 全文思维框架导图-帮助你快速回顾、梳理、总结
▶︎ 最后,我觉得还是有必要强调一点
本文中给出的DDD代码实现模型,也只是一个参考模型。而代码实现模型的设计,也与具体所采用的技术体系有一定关联。在本文所展示的案例中,我们使用了Spring Boot、Spring Cloud Stream等Spring家族中的开发框架,来开发DDD应用程序。
而如果你使用Axon这种基于事件溯源模式的DDD开发框架,那么在代码实现模型中,就需要引入用于事件分发和存储的Gateway、EventStore等组件,而位于基础设施中的传统数据持久化组件,可能就不一定会被使用到。
当然,基于我们今天介绍的内容,相信你并不难对这套DDD代码实现模型进行扩展。DDD作为一种系统建模方法论,也存在一些诸如分层架构、整洁架构、六边形架构等多种架构风格。
针对每种架构风格,我们都需要设计对应的代码实现模型。
而基于本文中介绍的内容,通过对DDD中各个核心概念与实现模型之间进行合理的映射,我在文中提供了一套设计代码实现模型的系统方法,从而帮助你可以应对不同架构风格的实现要求。
这也是本文的核心价值所在。