Skip to content

服务和数据在微服务各层的协作

DDD分层架构确定了微服务的总体架构。在微服务代码模型里,我们根据领域模型里领域对象的属性和依赖关系,将领域对象进行了分层,定义了与之对应的代码对象和代码目录结构。微服务内的主要对象有服务和实体等,它们共同协作完成业务逻辑。

服务视图

在微服务内有很多不同类型的服务,它们的实现方式不同,承担的职能也不同。它们连接微服务内不同的层,实现微服务之间的服务访问和协作。下面我们来分析一下这些服务的调用、组合和封装关系以及它们之间的依赖关系。

服务的类型

我们先来回顾一下DDD分层架构中的服务。按照分层架构设计出来的微服务,其内部主要有facade接口服务、应用服务、领域服务和基础服务。各层服务的主要功能和职责如下。

[facade接口服务]:位于用户接口层,包括接口和实现两部分。用于处理用户发送的RESTful请求和解析用户输入的配置文件等,并将数据传递给应用层。完成应用服务封装,将DO组装成DTO,并将数据传递给前端应用。

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

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

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

### 服务的调用

微服务的服务调用包括三类主要应用场景:微服务内跨层服务调用、微服务之间服务调用和领域事件驱动

  1. 微服务内跨层服务调用

微服务架构往往采用前后端分离的设计模式。前端应用实现前端页面逻辑,后端微服务实现核心领域逻辑,前后端应用分别独立部署。前端应用调用发布在API网关上的facade接口服务,facade接口服务定向到应用服务。

在微服务内,应用服务作为服务的组织者和编排者,它的服务调用有两种路径。

第一种是应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过工厂服务和仓储接口,访问仓储实现获取持久化数据对象,完成实体构建和数据初始化。

第二种是应用服务直接调用仓储服务。这种方式主要针对类似缓存或文件等类型的基础层数据访问,或者涉及多表关联的复杂数据查询操作。这些数据查询类操作,由于没有太多需要进行业务规则控制的领域逻辑,所以不需要经过领域层。

  1. 微服务之间的服务调用

对于实时性要求高的场景,微服务中应用服务可以通过API网关,访问其他微服务的应用服务,采用同步方式实现数据强一致性。

注意,在涉及跨微服务的数据新增和修改操作时,你需要关注分布式事务,保证数据的强一致性。但是这样微服务之间的依赖和耦合度就比较高了,也会影响应用的性能,所以一般优先选择领域事件驱动的数据最终一致性机制。

  1. 领域事件驱动

领域事件驱动是一种特殊的、异步化的调用方式,它包括微服务内和微服务之间的领域事件。微服务内的领域事件通过事件总线完成聚合之间的异步处理。微服务之间的领域事件通过消息中间件完成。

如果发生领域事件,当业务逻辑处理完成后,可调用事件发布服务,完成事件发布。

事件订阅服务接收到订阅的主题数据时,会调用事件处理领域服务,完成进一步的业务操作。

对于实时性要求不高的场景,建议优先采用领域事件驱动设计方式,通过异步方式实现数据最终一致性。

### 服务的封装与组合

微服务的服务是从领域层逐级向上封装、组合和暴露的。

  1. 基础层

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

  1. 领域层

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

实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,具体实现形式是实体类中的方法。实体是微服务内的原子业务对象,在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力,这是一种面向对象的编程方法。实体方法不会过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。

DDD提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。你也可以认为领域服务是介于实体和应用服务之间的薄薄的一层。它的主要职能是实现领域层复杂核心领域逻辑的组合和封装。

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

  1. 应用层

应用层主要面向前端应用和用户,根据前端用例和流程要求,通过服务组合和编排实现粗粒度的业务行为。应用层主要服务形态有:应用服务和事件订阅服务。

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

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

另外,应用层也可以完成微服务之间的事件发布和订阅操作。

为了实现微服务内聚合的解耦,原则上我们应该尽量避免聚合之间的领域服务直接调用和聚合之间的数据库表关联。聚合之间的服务调用和数据交互,可通过应用服务完成。
  1. 用户接口层

用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。用户接口层的主要服务形态是facade接口服务。

facade接口服务处理前端发送的RESTful请求和解析用户输入的配置文件等,将数据传递给应用层。或者获取应用服务的数据后,进行数据组装,向前端提供数据服务。

facade接口服务分为接口和实现两个部分,完成服务定向。通过assembler组装器,完成DO与DTO数据的转换和组装,完成前端应用与应用层数据的转换和交换。

facade接口服务本质上就是端口适配器架构模型中的适配器,面向前端应用和用户提供主动适配。

### 两种分层架构的服务依赖关系

现在我们回顾一下DDD分层架构。分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。

根据耦合的紧密程度,分层架构可以分为两种:松散分层架构和严格分层架构。在松散分层架构中,任何层都可以与其任意下方的层发生依赖。在严格分层架构中,任何层只能与位于其直接下方的层发生依赖。

下面我们来详细分析和比较一下这两种分层架构。

  1. 松散分层架构的服务依赖

在松散分层架构中,领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。在松散分层架构中,只需要满足上层服务依赖下层的要求就可以了,可以跨层调用,无须逐级封装。因此下层服务可以快速跨级暴露给上层。

但这种松散的服务访问方式存在一些问题。

1)容易暴露领域层核心业务的实现逻辑。

2)当实体方法或领域服务发生服务变更时,由于下层服务同时被多层服务调用和组合,不容易找出哪些上层服务调用和组合了它,不方便通知和修改所有的服务调用方。

3)如果应用服务过多地直接访问实体或实体的方法,就很容易在应用层沉淀太多的领域逻辑。如果过多地封装了同一个聚合的多个实体,就容易混淆应用层和领域层的边界。如果封装的是不同聚合的实体,则会让不同的聚合在应用层发生耦合,不利于微服务架构的演进。

如图17-4所示,在松散分层架构中,实体A的方法可以越过领域服务,在应用层组合和封装成应用服务aAppService,然后直接暴露给用户接口层aFacade。

  1. 严格分层架构的服务依赖

在严格分层架构中,每一层服务只能向其直接上一层提供服务。虽然实体、实体方法和领域服务都在领域层,但实体和实体方法只能暴露给领域服务,领域服务只能暴露给应用服务。

在严格分层架构中,如果需要跨层调用服务,下层服务需要在上层封装后,才可以提供跨层服务调用。比如,实体方法如果需要向应用服务提供服务,它需要先封装成领域服务。

这是因为通过封装你可以避免将核心业务逻辑的实现暴露给外部,将实体和方法封装成领域服务,也可以避免在应用层沉淀过多的本该属于领域层的核心业务逻辑,避免应用层变得臃肿,导致领域模型失焦。

此外,当服务发生变更时,由于服务只被紧邻上层的服务调用和组合,你只需要逐级告知紧邻的上层就可以了,其服务可管理性优于松散分层架构。

在严格分层架构中,服务需要逐层组合和封装。虽然这种方式会增加封装的工作量,但是服务边界却很清晰,服务的依赖关系也相对可控。

数据视图

在DDD中有很多实体和数据对象,这些对象分布在不同的层里。它们在不同的阶段有不同的形态,分别承担不同的职能。

我们先来看一下微服务内有哪些类型的数据对象。

▪数据持久化对象(Persistent Object,PO),与数据库结构一一映射,它是数据持久化过程中的数据载体。

▪领域对象(Domain Object,DO),微服务运行时核心业务对象的载体,DO一般包括实体或值对象。

▪数据传输对象(Data Transfer Object,DTO),用于前端应用与微服务应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体。

▪视图对象(View Object,VO),用于封装展示层指定页面或组件的数据。

这些数据对象又是如何协作和转换的?

  1. 基础层

微服务基础层的主要数据对象是PO。在设计时,我们需要先建立DO和PO的映射关系。大多数情况下DO和PO是一一对应的。但也有DO和PO多对多的情况,在这种情况下,在DO和PO数据转换时,需要进行数据重组。对于DO对象较复杂的数据转换操作,你可以在聚合内用工厂模式来实现。

当DO数据需要持久化时,先将DO转换为PO对象,由仓储实现服务完成数据库持久化操作。

当DO需要构建和数据初始化时,由仓储实现服务先从数据库获取PO对象,将PO转换为DO后,再完成DO数据构建和初始化。

  1. 领域层

领域层主要是DO对象。DO是实体和值对象的数据和业务行为载体,承载着基础的核心业务逻辑,多个依赖紧密的DO对象构成聚合。领域层DO对象在持久化时需要转换为PO对象。

  1. 应用层

应用层主要对象有DO对象,但也可能会有DTO对象。应用层在进行不同聚合的领域服务编排时,一般建议采用聚合根ID的引用方式,应尽量避免不同聚合之间的DO对象直接引用,避免聚合之间产生依赖。

在涉及跨微服务的应用服务调用时,在调用其他微服务的应用服务前,DO会被转换为DTO,完成跨微服务的DTO数据组装,因此会有DTO对象。

在前端调用后端应用服务时,用户接口层先完成从DTO到DO的转换,然后以DO作为应用服务的参数,传导到领域层完成业务逻辑处理。

  1. 用户接口层

用户接口层主要完成DO和DTO的互转,以及微服务与前端应用的数据交互和转换。

facade接口服务在完成后端应用服务封装后,会对多个DO对象进行组装,转换为DTO对象,向前端应用完成数据转换和传输操作。

facade接口服务在接收到前端应用传入的DTO后,会先完成从DTO向多个DO对象的转换,再调用后端应用服务完成业务逻辑处理。

  1. 前端应用

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

数据转换的主要目的是实现各层解耦,以保证领域模型的稳定,也是为了让微服务具有更强的扩展能力和适配能力。但每一次数据转换都是以性能作为代价,在设计时需要在性能和扩展能力之间找到平衡。

小结

在软件开发过程中,我们需要严格遵守各层服务和数据的职责要求,各据其位,各司其职,这样才能保证核心领域模型的稳定,实现解耦,同时也可以灵活应对外部需求的快速变化。

Released under the MIT License.