【DDD】代码篇

219 阅读15分钟

1 分层架构

image.png

  • 用户接口层:面向前端提供服务适配,面向资源层提供资源适配。这一层聚集了接口适配相关的功能。
  • 应用层职责:实现服务组合和编排,适应业务流程快速变化的需求。这一层聚集了应用服务和事件相关的功能。
  • 领域层:实现领域的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和事件等领域对象,以及它们组合所形成的业务能力。
  • 基础层:贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力。

2 代码模型

DDD 并没有给出标准的代码模型,不同的人可能会有不同理解。下面要说的这个微服务代码模型是我经过思考和实践后建立起来的,主要考虑的是微服务的边界、分层以及架构演进。

image.png

2.1 微服务一级目录结构

微服务一级目录是按照 DDD 分层架构的分层职责来定义的。从下面这张图中,我们可以看到,在代码模型里分别为用户接口层、应用层、领域层和基础层,建立了 interfaces、application、domain 和 infrastructure 四个一级代码目录。

image.png

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

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

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

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

2.1.1 用户接口层

Interfaces 的代码目录结构有:assembler、dto 和 façade 三类。

image.png

  • Assembler:实现 DTO 与领域对象之间的相互转换和数据交换。一般来说 Assembler 与 DTO 总是一同出现。
  • Dto:它是数据传输的载体,内部不存在任何业务逻辑,我们可以通过 DTO 把内部的领域对象与外界隔离。
  • Facade:提供较粗粒度的调用接口,将用户请求委派给一个或多个应用服务进行处理。
2.1.2 应用层

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

image.png

Event(事件):这层目录主要存放事件相关的代码。它包括两个子目录:publish 和 subscribe。前者主要存放事件发布相关代码,后者主要存放事件订阅相关代码(事件处理相关的核心业务逻辑在领域层实现)。这里提示一下:虽然应用层和领域层都可以进行事件的发布和处理,但为了实现事件的统一管理,我建议你将微服务内所有事件的发布和订阅的处理都统一放到应用层,事件相关的核心业务逻辑实现放在领域层。通过应用层调用领域层服务,来实现完整的事件发布和订阅处理流程。Service(应用服务):这层的服务是应用服务。应用服务会对多个领域服务或外部应用服务进行封装、编排和组合,对外提供粗粒度的服务。应用服务主要实现服务组合和编排,是一段独立的业务逻辑。你可以将所有应用服务放在一个应用服务类里,也可以把一个应用服务设计为一个应用服务类,以防应用服务类代码量过大。

2.1.1 领域层

Domain 是由一个或多个聚合包构成,共同实现领域模型的核心业务逻辑。聚合内的代码模型是标准和统一的,包括:entity、event、repository 和 service 四个子目录。

image.png

Aggregate(聚合):它是聚合软件包的根目录,可以根据实际项目的聚合名称命名,比如权限聚合。在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。聚合内实现高内聚的业务逻辑,它的代码可以独立拆分为微服务。以聚合为单位的代码放在一个包里的主要目的是为了业务内聚,而更大的目的是为了以后微服务之间聚合的重组。聚合之间清晰的代码边界,可以让你轻松地实现以聚合为单位的微服务重组,在微服务架构演进中有着很重要的作用。

Entity(实体):它存放聚合根、实体、值对象以及工厂模式(Factory)相关代码。实体类采用充血模型,同一实体相关的业务逻辑都在实体类代码中实现。跨实体的业务逻辑代码在领域服务中实现。 Event(事件):它存放事件实体以及与事件活动相关的业务逻辑代码。

Service(领域服务):它存放领域服务代码。一个领域服务是多个实体组合出来的一段业务逻辑。你可以将聚合内所有领域服务都放在一个领域服务类中,你也可以把每一个领域服务设计为一个类。如果领域服务内的业务逻辑相对复杂,我建议你将一个领域服务设计为一个领域服务类,避免由于所有领域服务代码都放在一个领域服务类中,而出现代码臃肿的问题。领域服务封装多个实体或方法后向上层提供应用服务调用。

Repository(仓储):它存放所在聚合的查询或持久化领域对象的代码,通常包括仓储接口和仓储实现方法。为了方便聚合的拆分和组合,我们设定了一个原则:一个聚合对应一个仓储。

特别说明:按照 DDD 分层架构,仓储实现本应该属于基础层代码,但为了在微服务架构演进时,保证代码拆分和重组的便利性,我是把聚合仓储实现的代码放到了聚合包内。这样,如果需求或者设计发生变化导致聚合需要拆分或重组时,我们就可以将包括核心业务逻辑和仓储代码的聚合包整体迁移,轻松实现微服务架构演进。

2.1.1 基础层

Infrastructure 的代码目录结构有:config 和 util 两个子目录。

image.png Config:主要存放配置相关代码。

Util:主要存放平台、开发框架、消息、数据库、缓存、文件、总线、网关、第三方类库、通用算法等基础代码,你可以为不同的资源类别建立不同的子目录。

3 领域->代码模型落地考量

从领域模型到微服务落地,我们还需要做进一步的设计和分析。事件风暴中提取的领域对象,还需要经过用户故事或领域故事分析,以及微服务设计,才能用于微服务系统开发。这个过程会比事件风暴来的更深入和细致。主要关注内容如下:

  1. 分析微服务内有哪些服务?
  2. 服务所在的分层?
  3. 应用服务由哪些服务组合和编排完成?
  4. 领域服务包括哪些实体的业务逻辑?
  5. 采用充血模型的实体有哪些属性和方法?
  6. 有哪些值对象?
  7. 哪个实体是聚合根等?
  8. 最后梳理出所有的领域对象和它们之间的依赖关系,我们会给每个领域对象设计对应的代码对象,定义它们所在的软件包和代码目录。

这个设计过程建议参与的角色有:DDD 专家、架构师、设计人员和开发经理。

4 数据流转

分层架构确定了微服务的总体架构,微服务内的主要对象有服务和实体等,它们一起协作完成业务逻辑。那在运行过程中,这些服务和实体在微服务各层是如何协作的呢?

4.1 服务类型

按照分层架构设计出来的微服务,其内部有 Facade 服务、应用服务、领域服务和基础服务。各层服务的主要功能和职责如下。

Facade 服务:位于用户接口层,包括接口和实现两部分。用于处理用户发送的 Restful 请求和解析用户输入的配置文件等,并将数据传递给应用层。或者在获取到应用层数据后,将 DO 组装成 DTO,将数据传输到前端应用。

应用服务:位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果拼装,对外提供粗粒度的服务。

领域服务:位于领域层。领域服务封装核心的业务逻辑,实现需要多个实体协作的核心领域逻辑。它对多个实体或方法的业务逻辑进行组合或编排,或者在严格分层架构中对实体方法进行封装,以领域服务的方式供应用层调用。

基础服务:位于基础层。提供基础资源服务(比如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务应用逻辑的影响。基础服务主要为仓储服务,通过依赖倒置提供基础资源服务。领域服务和应用服务都可以调用仓储服务接口,通过仓储服务实现数据持久化。

4.2 服务调用

我们看一下下面这张图。微服务的服务调用包括三类主要场景:

  1. 微服务内跨层服务调用
  2. 微服务之间服务调用
  3. 领域事件驱动

image.png

4.2.1 微服务内跨层服务调用

微服务架构下往往采用前后端分离的设计模式,前端应用独立部署。前端应用调用发布在 API 网关上的 Facade 服务,Facade 定向到应用服务。应用服务作为服务组织和编排者,它的服务调用有这样两种路径:

  • 第一种是应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过仓储服务获取持久化数据对象,完成实体数据初始化。
  • 第二种是应用服务直接调用仓储服务。这种方式主要针对像缓存、文件等类型的基础层数据访问。这类数据主要是查询操作,没有太多的领域逻辑,不经过领域层,不涉及数据库持久化对象。
4.2.2 微服务之间服务调用

微服务之间的应用服务可以直接访问,也可以通过 API 网关访问。由于跨微服务操作,在进行数据新增和修改操作时,你需关注分布式事务,保证数据的一致性。

4.2.3 领域事件驱动

领域事件驱动包括微服务内和微服务之间的事件。

  • 微服务内通过事件总线(EventBus)完成聚合之间的异步处理。
  • 微服务之间通过消息中间件完成。异步化的领域事件驱动机制是一种间接的服务访问方式。

当应用服务业务逻辑处理完成后,如果发生领域事件,可调用事件发布服务,完成事件发布。当接收到订阅的主题数据时,事件订阅服务会调用事件处理领域服务,完成进一步的业务操作。

4.3 服务封装与组合

我们看一下下面这张图。微服务的服务是从领域层逐级向上封装、组合和暴露的。

image.png

基础层

基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。

领域层

领域层实现核心业务逻辑,负责表达领域模型业务概念、业务状态和业务规则。主要的服务形态有实体方法和领域服务。

  • 实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力。不必过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。
  • DDD 提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。

对于严格分层架构,如果单个实体的方法需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务。

应用层

应用层用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。

应用层的主要服务形态有:应用服务、事件发布和订阅服务。应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。除了完成服务的组合和编排外,应用服务内还可以完成安全认证、权限校验、初步的数据校验和分布式事务控制等功能。

为了实现微服务内聚合之间的解耦,聚合之间的服务调用和数据交互应通过应用服务来完成。原则上我们应该禁止聚合之间的领域服务直接调用和聚合之间的数据表关联。

用户接口层

用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。它处理前端发送的 Restful 请求和解析用户输入的配置文件等,将数据传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。

主要服务形态是 Facade 服务。Facade 服务分为接口和实现两个部分。完成服务定向,DO 与 DTO 数据的转换和组装,实现前端与应用层数据的转换和交换。

4.4 数据对象视图

我们结合下面这张图,看看微服务各层数据对象的职责和转换过程。

image.png

基础层

基础层的主要对象是 PO 对象。我们需要先建立 DO 和 PO 的映射关系。当 DO 数据需要持久化时,仓储服务会将 DO 转换为 PO 对象,完成数据库持久化操作。当 DO 数据需要初始化时,仓储服务从数据库获取数据形成 PO 对象,并将 PO 转换为 DO,完成数据初始化。大多数情况下 PO 和 DO 是一一对应的。但也有 DO 和 PO 多对多的情况,在 DO 和 PO 数据转换时,需要进行数据重组。

领域层

领域层的主要对象是 DO 对象。DO 是实体和值对象的数据和业务行为载体,承载着基础的核心业务逻辑。通过 DO 和 PO 转换,我们可以完成数据持久化和初始化。

应用层

应用层的主要对象是 DO 对象。如果需要调用其它微服务的应用服务,DO 会转换为 DTO,完成跨微服务的数据组装和传输。用户接口层先完成 DTO 到 DO 的转换,然后应用服务接收 DO 进行业务处理。如果 DTO 与 DO 是一对多的关系,这时就需要进行 DO 数据重组。

用户接口层

用户接口层会完成 DO 和 DTO 的互转,完成微服务与前端应用数据交互及转换。Facade 服务会对多个 DO 对象进行组装,转换为 DTO 对象,向前端应用完成数据转换和传输。

前端应用

前端应用主要是 VO 对象。展现层使用 VO 进行界面展示,通过用户接口层与应用层采用 DTO 对象进行数据交互。