当前位置: 首页 > 后端技术 > Java

DDD的概念复杂且难以理解,实践中如何设计代码实现模型?

时间:2023-04-02 09:41:48 Java

今天想跟大家聊聊。DDD的概念复杂且难以理解。如何在实践中设计代码实现模型。关于这个话题,我先说说整体的框架和思路。我打算结合两部分来和大家分享。相信仔细阅读每一部分,你或多或少都会有所收获。以下内容预计1分钟左右快速阅读:第一部分,方法,旨在详细介绍DDD包含的几个核心概念,以及围绕这些概念构建的DDD代码实现模型的组成和结构.下半场,实用篇,进一步思考。我接着说,按照前面的内容,要让这些代码实现模型真正落地,还需要结合具体的应用场景。我将着重详述DDD代码实现模型的设计方法,并给出具体的案例分析。随着业务系统复杂度的不断提高和微服务架构等分布式技术系统的普及,领域驱动设计(DomainDrivenDesign,DDD)逐渐成为系统建模领域的主流设计思想和模型。DDD中引入了限界上下文、聚合、实体、值对象、领域事件、资源库、应用服务等一系列核心概念。有了这些概念,开发人员就可以进行系统设计和实现。但是DDD中的这些概念都比较抽象,甚至有些晦涩难懂。靠的是相似或者相似的问题,我认为本质上,对于复杂难懂的概念的理解和把握,我们一开始不必太纠结于这些概念本身,而是可以将它们对应到具体的实现模型上事实上。通过两者之间的合理映射,促进了对概念本身的理解,如下图所示。在这里多说一句,即使是其他技术领域的朋友,也可能遇到过类似的问题,有共性。希望看完今天的分享,能或多或少的帮助到你,给你一些启发和思考。在上图中,我们一方面试图将复杂的概念映射到实施模型。另一方面,基于对实现模型的把握,可以反推复杂概念的理解程度,从而更好地掌握这些概念。这足以看出实践可以带来真知,只有设计好实现模型后,才能真正掌握这些概念,并将其应用到各种具体场景中。这是一种行之有效的方法。那么问题来了。在日常的开发过程中,如何保证DDD真正落地,如何将这些抽象的概念转化为具体的代码模式,就是我们今天要讨论的内容。01?如果要设计代码实现模型,是不是一定要了解DDD中的这些核心概念?总的来说,DDD为业务建模和软件设计提供了一种方法论。DDD认为一个好的系统架构应该是技术架构和业务架构融合的结果,开发者不能跳出业务领域来设计技术架构。为了实现这个目标,DDD提出了一组核心概念,如图1所示。我们先来看第一个核心概念,比较难理解,即限界上下文(BoundaryContext)。在DDD中,当我们将业务域拆分成多个子域时,有界上下文定义了子域的业务边界,实现了子域之间的隔离,如图2所示。有了有界上下文,我们需要围绕业务场景设计领域模型对象(DomainModelObject)。领域模型对象包含丰富的业务逻辑和操作行为,这与传统的仅包含数据属性的数据对象有着根本的区别。因此,领域模型对象是我们在应用DDD时最应该关注的一组对象,也是最难掌握的一组对象。在DDD中,领域模型对象包括聚合、实体和值对象三类,每一类都有自己的特点。与领域模型对象相比,领域事件诞生较晚,但也是领域模型的重要组成部分,因为现实中的很多场景都可以抽象为事件(Event),如图4所示。在DDD中,领域事件可以用于有效地传播业务状态变化,并在单个有界上下文或多个上下文之间响应这些状态变化。业务领域的各种状态变化最终都需要存储起来。为此,DDD提供了一个统一的业务数据访问入口,即Repository。通过资源库,我们可以实现对各种领域对象的持久化操作,如图5所示。最后介绍一下应用服务的概念。应用服务包括命令(Command)服务和查询(Query)服务,本质上起到解耦和协调的作用,保证各个领域模型对象之间的交互和协作。因此,当涉及到多个限界上下文之间的交互时,我们需要关注应用服务。如图6所示。02?概念复杂,不易理解。如果要在实际业务场景中实现,需要引入DDD代码实现模型。关于DDD中的核心概念,这里简单介绍一下。接下来就是讨论一个所有开发者都必须面对的问题。题目,即如何在实际开发过程中实现这些复杂难懂的概念?这就需要引入DDD代码实现模型。??如果要设计一个代码实现模型,首先要搞清楚它由哪些部分组成?再好的设计方法,能转化为可运行的代码才是王道,对于DDD来说更是如此。遗憾的是,目前业界还没有一套统一的标准和规范来说明如何将这些概念落地,这让我们在具体的开发过程中常常感到无所适从。为此,本文特地提炼出一套DDD代码实现模型。接下来,让我们讨论一下DDD代码实现模型的基本概念和组织结构。??说代码实现模型之前,先搞清楚什么是实现模型。说到模型(Model),业界主流方法论认为分为领域模型、设计模型和代码模型三种,如图7所示。关于领域模型,我们已经在以前的内容。在DDD中,聚合、实体、值对象、领域事件等,都可以归于这个模型的范畴。设计模型(DesignModel)可以分为两个部分:边界模型和内部模型。边界模型定义了系统边界并抽象出系统集成和交互方案。内部模型细化边界模型,在明确系统边界的前提下,实现对系统内部模块和组件的抽象和构建。因此,在DDD中,我们往往从限界上下文的角度出发,进行设计模型的构建,如下图所示。最后,代码模型为现实世界的解决方案提供了一个可执行的系统环境。我们可以通过在领域模型和设计模型中嵌入代码来构建代码模型。这个模型是将DDD的各种复杂概念转化为可执行代码的关键,也是我们今天要讨论的主要内容。显然,领域模型、设计模型和代码模型之间存在层次依赖关系,如图9所示。首先,领域模型代表了领域内在的业务;设计模型指向领域模型,着重于对外接口和交互关系的承诺;代码模型提供了完整的实现过程,是设计模型的细化。正是通过这三种模型的融合,完成了从实际问题到最终可实施的实施方案的演化。??DDD代码实现模型应该包含哪些部分?对于DDD代码实现模型的讨论,我们也会遵循以上三种模型的整合过程。结合DDD中的各种核心概念,我们梳理出DDD代码实现模型的组成结构,如图10所示。在上图中,我们可以清晰的看到DDD代码实现模型的四个组成部分,分别面向域对象、应用程序服务、基础设施和上下文集成。说到这里,你可能会问,为什么我们要这样设计DDD代码实现模型呢?我们知道,一个完整的DDD应用程序通常由多个限界上下文组成。因此,对于代码实现模型,我们需要关注两个维度,即:单个限界上下文实现过程中的代码模型和多个限界上下文之间集成过程中的代码模型。上图中,关于领域对象、应用服务和基础设施代码实现模型的讨论属于单一限界上下文的范畴,而上下文实现代码集成模型显然是面向多限界上下文的,如图11所示。通过前面内容的学习,相信大家对DDD代码实现模型的组成和结构有了清晰的认识。那么,在日常的开发过程中,我们应该如何设计这些代码实现模型呢?有没有具体案例可以参考?你可以先停下来想想这几个问题。03?如何设计DDD代码实现模型?在分析DDD代码实现模型的时候,我们需要梳理一下上一篇文章提到的四个组件的代码结构和依赖关系。对于代码结构,我们需要明确代码包的组成以及其中包含的技术组件。明确了包结构之后,依赖指的是我们需要进一步明确这些代码包与技术组件之间的交互。基于这两点,我们先来讨论领域对象的代码实现模型。??领域对象代码实现模型对于领域对象,我们通常用“领域”一词来命名代码包结构的顶层包。包结构下的所有技术组件都属于领域对象的范畴。具体来说,在DDD中,领域对象包括领域模型对象、领域事件、资源库以及应用服务中涉及的命令和查询对象。领域模型对象可以分为三类:聚合、实体和值对象。因此,在DDD的所有代码实现模型中,领域对象涉及的代码结构是最复杂的,可以分为两个层次,如图1所示。从图1可以看出,这里的“领域”代表整个领域对象,而“模型”代表领域模型对象。请注意两者在命名上的区别以及它们之间的隶属关系。领域对象是DDD代码实现模型的基础,包括核心业务逻辑的实现。??应用服务代码实现模型同样,对于应用服务,我们通常使用“application”来命名顶层包结构。应用服务包括查询服务和命令服务两大类,因此在命名上也使用“commandservice”和“queryservice”来区分子包,如图2所示。如图2所示,命令服务和查询服务依赖于命令对象和查询对象分别在域对象代码实现模型中,我们用虚线表示这种依赖程度。在DDD的代码实现模型中,应用服务可以说是交互关系最为复杂的代码模型。一方面,它需要将命令和查询操作分派给聚合对象等领域模型对象。另一方面,它还需要分别与基础设施和其他有界上下文进行交互。关于后者,我们将在讨论案例研究时进一步展开。??基础设施代码实现模型其实所谓的基础设施是指DDD应用中使用的各种具体的技术、工具和框架。常见的基础设施组件主要包括这几个方面:数据持久化(Persistence)、消息通信(Messaging)、系统配置(Config)、安全控制(Security)。因此,基础设施的包结构不是固定的,而是取决于具体的技术开发,需要灵活组织。这里是一个常见的包结构,如图3所示。对于infrastructure,我们使用“infrastructure”来命名这个包结构。图3上图中需要注意的一点是,代表数据持久化的“persistence”包和代表消息通信的“messaging”包在基础设施代码实现模型中是最常见的,因为它们对应的是领域对象。存储库和域事件。在DDD中,资源库和领域事件的定义位于领域对象代码实现模型中,与具体的实现技术无关。具体实现技术相关的持久化和消息通信位于基础代码实现模型中。这体现了领域对象和实现技术相互分离的设计原则。??上下文集成代码实现模型最后,我们来讨论一下上下文集成代码实现模型。需要注意的是,这种模式是最难实现的,因为它涉及到多种系统集成技术体系。对于这个代码实现模型,我们首先需要明确的是它是面向多个有界上下文的,所以我们需要考虑数据的流向,也就是所谓的入站(Inbound)数据和出站(Outbound)数据。一方面,有界上下文需要公开访问条目以供其他上下文使用。从当前上下文来看,这是一个入站操作。而当一个context向外部context发起请求时,这就是一个outbound操作,如图4所示。图4在代码实现模型的设计中,我们也会使用“inbound”和“outbound”来命名包结构。那么这两个包结构下应该包含哪些技术组件呢?我们先来讨论“outbound”包结构,如图5所示。图中,“rest”包中的RESTAPI将外部请求转换为内部的Command和Query对象,提交给应用服务进行处理。在这个转换过程中,通常需要引入特殊的DTO(DataTransferObject,数据传输对象)对象和汇编器(Assembler)对象。图5同时,“eventpublisher”包中的事件发布者(EventPublisher)用于向外部限界上下文发布领域事件。接下来,我们讨论“入站”数据包结构。在有界上下文中,有两种主要类型的入站数据操作。一种是反腐败层(Anti-CorruptionLayer,ACL),用于向远程RESTAPI发起请求并获取结果。另一类是事件处理器(EventHandler),用于完成对领域事件的响应,如图6所示。图6是基于上下文的集成流程。两个上下文中的“inbound”和“outbound”包结构中包含的技术组件实际上是一一对应的,如图7所示。可以看出,“acl”和“eventhandler”在一个bounded上下文“inbound”对应于另一个有界上下文“outbound”中的“rest”和“eventpublisher”。图7至此,DDD中的四类代码实现模型已经介绍完毕。在接下来的内容中,我们将结合具体的应用场景,通过案例分析将这些代码实现模型付诸实践。基于此案例,您可以将本文介绍的所有内容与日常开发流程联系起来,进一步掌握将模型转化为具体代码的实现方法和技巧。04?DDD代码实现模型案例分析在现实世界中,工单处理是一种非常普遍的业务需求。工单的发起通常是因为用户需要咨询或投诉订单。在这种场景下,基于DDD的设计方式,我们可以分离出三个限界上下文:Ticket、Staff、Order。在这三个上下文中,Ticket上下文将分别与Staff和Order这两个上下文集成,创建一个工单应用,如图8所示。请注意,图中显示的是Ticket上下文,它有两种不同的上下文集成方式.对于Staffcontext,Ticketcontext会使用RESTAPI完成工单中客服数据的获取。对于Ordercontext,使用的是领域事件,即一旦Order的状态发生变化,Ordercontext会将相应的领域事??件发送给Ticketcontext。图8??Ticketcontext代码实现模型示例显然,对于这个场景,Ticketcontext既有Inbound操作,也有Outbound操作。因此,它的代码实现模型是最完整的,如图9所示。在图9的上图中,我们使用IDEA这个开发工具,以及SpringBoot这个具体的开发框架,构建了Ticket的代码实现模型有界上下文。我们可以清楚的看到,四种DDD代码实现模型的表示就是五种顶层代码包结构。其中,上下文集成代码实现模型包括“入站”和“出站”两种代码包。我们再对这些顶层代码包结构进行展开,得到如图10所示的子代码包结构。图10(上下滑动查看)上图中所有的子代码包结构都已经给出了相应的说明在前面的内容中,这里不再赘述。在Ticket上下文中,命令服务TicketCommandService完成Staff服务的上下文集成。这时候就用到了防腐层ACL组件。示例代码如下。可以看出,这里使用了ACL组件AclStaffService来发起对Staff服务的远程调用,然后将返回结果填充到command对象中,并创建一个Ticket聚合。最后我们通过TicketRepository完成了聚合对象的持久化操作。上述图11中的AclStaffService完成了对Staffcontext提供的RESTAPI的调用,示例代码如下。这里使用Spring自带的RestTemplate模板工具类来完成对远程HTTP端点的访问操作。图12??Staff上下文代码实现模型示例在Staff上下文中,我们需要完成上述RESTAPI的构建,其代码工程结构如下图所示。可以看出,与Ticket上下文相比,Staff上下文的代码结构更简单,因为上下文只需要对外提供“出站”包,基础设施部分只需要完成领域对象的持久化操作.图13??Ordercontext代码实现模型示例最后我们来到Orderboundedcontext,它的代码实现模型是这样的,大家可以一起看看。在图14中,我们知道了Order上下文,并提供了Order数据的领域事件发布机制,因此其“outbound”包中包含了发布领域事件的“eventpublisher”子包,并提供了一个OrderEventPublisherService,如下所示。这里的图15通过SpringCloudStream实现领域事件的发布。在Ticket上下文中,我们也可以基于SpringCloudStream来监听和消费该字段的事件。示例代码如下。图16请注意,上面的OrderUpdatedEventHandler位于Ticket上下文“inbound”包的“eventhandler”子包中。这些具体实现代码的解释不是本文的重点。可以参考作者在Github上的案例代码进行系统学习:https://github.com/tianminzhe...。05?总结与延伸思考今天的分享到这里就结束了。本文内容详细解答了开发者在实现DDD应用中遇到的一个核心问题,即如何构建DDD代码实现模型。之所以讨论这个话题,是因为DDD中的很多概念都比较晦涩,业界也没有对这些概念如何实现提供统一的开发规范和标准。通过将DDD中各种复杂的概念与具体的代码实现模型进行映射,在帮助我们更好地理解这些概念的同时,也可以将它们直接应用到日常的开发过程中。通过本文内容的介绍,开发者可以根据自己的业务发展需求,设计出一套完整的DDD代码实现模型。这里还附上全文思维导图,帮助大家回顾整理思路。图17全文思维框架图——助你快速回顾、梳理、总结??最后,我认为有必要强调一下,本文给出的DDD代码实现模型只是一个参考模型。代码实现模型的设计也与采用的具体技术体系有关。本文案例中,我们使用SpringBoot、SpringCloudStream等Spring家族的开发框架开发DDD应用。而如果使用Axon这种基于事件源模型的DDD开发框架,那么在代码实现模型中,需要引入Gateway、EventStore等组件进行事件分发和存储,而传统的数据持久化组件在基础设施中,可能不一定被使用。当然,根据我们今天的介绍,相信大家扩展这套DDD代码实现模型并不难。DDD作为一种系统建模方法论,也有分层架构、整洁架构、六边形架构等多种架构风格。对于每一种架构风格,我们都需要设计相应的代码实现模型。基于本文介绍的内容,通过DDD中各个核心概念与实现模型的合理映射,本文提供了一套系统的代码实现模型设计方法,以帮助大家应对不同的建筑风格要求。这也是本文的核心价值。