Skip to content

如何用DDD设计微服务代码模型

在完成领域模型设计后,接下来我们就可以开始微服务的设计和落地了。在微服务落地前,首先要确定微服务的代码结构,也就是我下面要讲的微服务代码模型。

只有建立了标准的微服务代码模型和代码规范后,我们才可以将领域对象映射到代码对象,并将它们放入合适的代码目录结构中。标准的代码模型可以让项目团队成员更好地理解代码,根据统一的代码规范实现团队协作,也可以让微服务各层的业务逻辑互不干扰、分工协作、各据其位、各司其职,避免不必要的代码混淆,还可以让你在微服务架构演进时,轻松完成代码重构。

那微服务的代码结构到底是什么样子呢?我们又是依据什么来建立微服务的代码模型呢?这就是我们本章要重点解决的两个问题。

DDD分层架构与微服务代码模型

我们参考DDD分层架构模型来设计微服务代码模型。没错!微服务代码模型就是依据DDD分层架构模型设计出来的。

那为什么要选择DDD分层架构模型呢?

我们先简单回顾一下DDD分层架构模型,它包括用户接口层、应用层、领域层和基础层,分层架构各层的职责边界非常清晰,能有条不紊地分层协作。

▪用户接口层:面向前端用户提供服务和数据适配。这一层聚集了接口和数据适配相关的功能。

▪应用层:实现服务组合和编排,主要适应业务流程快速变化的需求。这一层聚集了应用服务和事件订阅相关的功能。

▪领域层:实现领域模型的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和事件等领域对象,通过各领域对象的协同和组合形成领域模型的核心业务能力。

▪基础层:它贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力。

领域模型的业务逻辑从领域层、应用层到用户接口层逐层组合和封装,对外提供灵活的服务。既实现了各层的分工和解耦,又实现了各层的协作。因此,毋庸置疑,DDD分层架构模型是微服务代码模型最合适的选择。

微服务代码模型

现在,我们来看一下,按照DDD分层架构模型设计出来的微服务代码模型到底长什么样子?

其实,DDD并没有给出标准的代码模型,不同的人可能会有不同理解,也会结合自己项目的情况进行个性化设计。下面要说的这个微服务代码模型是我经过思考和实践后建立起来的,主要考虑了微服务边界、聚合边界、分层、解耦和微服务的架构演进等因素。

一级代码目录

微服务一级目录是按照DDD分层架构的分层职责来定义的。

在微服务代码模型里,我们分别定义了用户接口层、应用层、领域层和基础层四层,并分别为它们建立了interfaces、application、domain和infrastructure四个一级代码目录。

这些代码目录的职能和代码形态如下。

▪interfaces(用户接口层):它主要存放用户接口层与前端应用交互、数据转换和交互相关的代码。前端应用通过这一层的接口,从应用服务获取前端展现所需的数据。处理前端用户发送的REStful请求,解析用户输入的配置文件,并将数据传递给application层。数据的组装、数据传输格式转换以及facade接口封装等代码都会放在这一层目录里。

▪application(应用层):它主要存放与应用层服务组合和编排相关的代码。应用服务向下基于微服务内的领域服务或外部微服务的应用服务,完成服务的组合和编排,向上为用户接口层提供各种应用数据支持服务。应用服务和事件等代码会放在这一层目录里。

▪domain(领域层):它主要存放与领域层核心业务逻辑相关的代码。领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑。聚合内的聚合根以及实体、方法、值对象、领域服务和事件等相关代码会放在这一层目录里。

▪infrastructure(基础层):它主要存放与基础资源服务相关的代码。为其他各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。

各层代码目录

下面我们一起来看一下用户接口层、应用层、领域层以及基础层各自的二级代码目录结构。

  1. 用户接口层

interfaces目录下的代码目录结构有assembler、dto和facade三类。

▪assembler:实现DTO与DO领域对象之间的相互转换和数据交换。一般来说,assembler与dto总是同时出现。

▪dto:它是前端应用数据传输的载体,不实现任何业务逻辑。我们可以面向前端应用将应用层或领域层的DO对象转换为前端需要的DTO对象,从而隐藏领域模型内部领域对象DO;也可以将前端传入的DTO对象转换为应用服务或领域服务所需要的DO对象。

▪facade:封装应用服务,提供较粗粒度的调用接口,或者将用户请求委派给一个或多个应用服务进行处理。

  1. 应用层

application的代码目录结构有event和service。

event(事件):这层目录主要存放事件相关的代码。它包括两个子目录:publish和subscribe。前者主要存放事件发布相关代码,后者主要存放事件订阅相关代码。事件处理相关的核心业务逻辑在领域层实现。

应用层和领域层都可以进行事件发布。为了实现事件订阅的统一管理,建议你将微服务内所有事件订阅的相关代码都统一放到应用层。事件处理相关的核心业务逻辑实现可以放在领域层。通过应用层调用领域层服务,来实现完整的事件订阅处理流程。

service(应用服务):这层的服务是应用服务。应用服务会对多个领域服务或其他微服务的应用服务进行封装、编排和组合,对外提供粗粒度的服务。你可以为每个聚合的应用服务设计一个应用服务类。

另外,在进行跨微服务调用时,部分DO对象需要转换成DTO,所以应用层可能也会有用户接口层的assembler和dto对象。这时,你可以根据需要增加assembler和dto代码目录结构。

对于多表关联的复杂查询,由于这种复杂查询不需要有领域逻辑和业务规则约束,因此不建议将这类复杂查询放在领域层的领域模型中。

你可以通过应用层的应用服务采用传统多表关联的SQL查询方式,也可以采用CQRS读写分离的方式完成数据查询操作。

  1. 领域层

domain下的目录结构是由一个或多个独立的聚合目录构成,每一个聚合是一个独立的业务功能单元,多个聚合共同实现领域模型的核心业务逻辑。

聚合内的代码模型是标准且统一的,它一般包括entity、event、repository和service四个子目录。

aggregate(聚合):它是聚合目录的根目录,你可以根据实际项目的聚合名称来命名,比如将聚合命名为"Person"。

聚合内实现高内聚的核心领域逻辑,聚合可以独立拆分为微服务,也可以根据领域模型的演变,在不同的微服务之间进行聚合代码重组。

将聚合所有的代码放在一个目录里的主要目的,不仅是为了业务的高内聚,也是为了未来微服务之间聚合代码重组的便利性。有了清晰的聚合代码边界,你就可以轻松地实现以聚合为单位的微服务拆分和重组。

聚合之间的松耦合设计和清晰的代码边界,在微服务架构演进中具有非常重要的价值。

聚合内可以定义聚合根、实体和值对象以及领域服务等领域对象,一般包括以下目录结构。

▪entity(实体):它存放聚合根、实体和值对象等相关代码。实体类中除了业务属性,还有业务行为,也就是实体类中的方法。如果聚合内部实体或值对象比较多,你还可以再增加一级子目录加以区分。

▪event(事件):它存放事件实体以及与事件活动相关的业务逻辑代码。

▪service(领域服务):它存放领域服务、工厂服务等相关代码。一个领域服务是由多个实体组合出来的一段业务逻辑。你可以将聚合内所有领域服务都放在一个领域服务类中。如果有些领域服务的业务逻辑相对复杂,你也可以将一个领域服务设计为一个领域服务类,避免将所有领域服务代码都放在一个领域服务类中而出现代码臃肿的问题。领域服务可以封装多个实体或方法供上层应用服务调用。

▪repository(仓储):它存放仓储服务相关的代码。仓储模式通常包括仓储接口和仓储实现服务。它们一起完成聚合内DO领域对象的持久化,或基于聚合根ID查询,完成聚合内实体和值对象等DO领域对象的数据初始化。另外,仓储目录还会有持久化对象PO,以及持久化实现逻辑相关代码,如DAO等。在仓储设计时有一个重要原则,就是一个聚合只能有一个仓储。

按照DDD分层架构,仓储本应该属于基础层。但为了在微服务架构演进时保证聚合代码重组的便利,这里将仓储相关代码也放到了领域层的聚合目录中。

这是因为聚合和仓储总是一对一的关系,将领域模型和仓储的代码组合在一起后,就是一个包含了领域层领域逻辑和基础层数据处理逻辑的聚合代码单元。一旦领域模型发生变化,当聚合需要在不同的限界上下文或微服务之间进行代码重组时,我们就可以以聚合代码包为单元,进行整体拆分或者迁移,轻松实现微服务架构演进。

虽然领域相关的业务逻辑代码和基础资源处理相关的代码都在一个聚合代码目录下,但是聚合的核心业务逻辑仍然是通过调用仓储接口来访问基础资源的仓储实现处理逻辑,所以这样不会影响业务逻辑与基础资源逻辑的依赖倒置设计。

  1. 基础层

infrastructure的代码目录结构有config和util两个子目录.

▪config:主要存放配置相关代码。

▪util:主要存放平台、开发框架、消息、数据库、缓存、文件、总线、网关、第三方类库和通用算法等基础代码。

你可以为不同的资源类别建立不同的子目录。

微服务总目录结构

完成一级和二级代码目录结构模型设计后,你就可以看到微服务代码模型目录结构的全貌了。这些代码虽然根据不同的职能分散到了不同的层和目录,但它们是在同一个微服务的工程内,作为一个微服务部署包进行整体发布和部署。

小结

我们根据DDD分层架构模型,建立了微服务的标准代码模型。在代码模型里面,各层的代码对象各据其位、各司其职,共同协作完成微服务的业务逻辑。

关于微服务代码模型我还需要强调两点内容。

第一点,聚合之间的代码边界一定要清晰。聚合之间的服务调用和数据关联应该尽可能松耦合和低关联,聚合之间的服务调用应该通过上层的应用层组合实现调用,原则上不允许聚合之间直接调用领域服务。这种松耦合的聚合代码关联,在以后业务发展和需求变更时,可以很方便地实现业务功能和聚合代码的重组,在微服务架构演进中将会起到非常重要的作用。

第二点,一定要有代码分层的概念。有了分层的思想后,写代码时一定要搞清楚代码的职责,将它放在职责对应的代码目录内。应用层代码主要完成服务组合和编排,以及聚合之间的协作,它是很薄的一层,不应该有核心领域逻辑代码。领域层是领域模型的业务的核心,领域模型的核心逻辑代码一定要在领域层实现。如果将核心领域逻辑代码放到应用层,你的基于DDD分层架构模型的微服务可能会慢慢变回原来紧耦合的传统三层架构,这样是不利于未来微服务架构的演进的。

Released under the MIT License.