在 DDD 分层架构和微服务代码模型里,我们根据领域对象的属性和依赖关系,将领域对象进行分层,定义了与之对应的代码对象和代码目录结构。分层架构确定了微服务的总体架构,微服务内的主要对象有服务和实体等,它们一起协作完成业务逻辑。
那在运行过程中,这些服务和实体在微服务各层是如何协作的呢❓
今天我们就来解剖一下基于 DDD 分层架构的微服务,看看它的内部结构到底是什么样的。
一、服务的协作
1.1 服务的类型
我们先来对分层架构中的服务进行一番回顾。按照分层架构设计而成的微服务,其内部涵盖了 Facade 服务、应用服务、领域服务以及基础服务。接下来,我们详细了解各层服务的主要功能与职责。
Facade 服务
:
该服务处于用户接口层,包含接口与实现两个部分。它主要承担着处理用户发送的 Restful 请求的任务,同时负责解析用户输入的配置文件等。在获取到相关数据后,将其传递给应用层。此外,当获取到应用层的数据时,Facade 服务会把 DO 组装成 DTO,进而将数据传输到前端应用。
应用服务
:
应用服务位于应用层。它的主要作用是表述应用以及用户行为。具体而言,负责服务的组合、编排和转发工作,并且要处理业务用例的执行顺序,以及对结果进行拼装。从对外提供服务的角度来看,应用服务提供的是粗粒度的服务。
领域服务
:
领域服务处于领域层。其核心功能在于封装核心的业务逻辑,实现那些需要多个实体协作的核心领域逻辑。在具体的实现过程中,领域服务会对多个实体或方法的业务逻辑进行组合或编排。另外,在严格分层架构中,它还会对实体方法进行封装,以领域服务的形式供应用层调用。
基础服务
:
基础服务位于基础层。它主要提供基础资源服务,比如数据库、缓存等相关服务。通过提供这些基础服务,实现了各层之间的解耦,有效降低了外部资源变化对业务应用逻辑所产生的影响。基础服务主要表现为仓储服务,通过依赖倒置的方式来提供基础资源服务。值得注意的是,领域服务和应用服务都能够调用仓储服务接口,并且借助仓储服务来实现数据的持久化。
1.2 服务的调用
我们看一下下面这张图。微服务的服务调用包括三类主要场景:微服务内跨层服务调用,微服务之间服务调用和领域事件驱动。
【图】微服务内跨层服务调用
在微服务架构中,常常会采用前后端分离的设计模式,前端应用能够独立部署。前端应用通过调用发布在 API 网关上的 Facade 服务,实现与后端的交互,而 Facade 服务则会定向到应用服务。
应用服务在整个架构中扮演着服务组织和编排者的角色,它的服务调用存在两种路径:
【第一种路径】:应用服务调用并组装领域服务。在这个过程中,领域服务会对实体和实体方法进行组装,以此来实现核心领域逻辑。同时,领域服务需要通过仓储服务获取持久化数据对象,从而完成实体数据的初始化工作。
【第二种路径】:应用服务直接调用仓储服务。这种方式主要适用于对缓存、文件等基础层数据的访问。这类数据操作大多为查询操作,不存在过多的领域逻辑,所以无需经过领域层,也不涉及数据库持久化对象。
在微服务之间的服务调用方面,微服务之间的应用服务既可以直接进行访问,也能够通过 API 网关进行访问。不过,由于是跨微服务操作,当进行数据新增和修改操作时,就需要特别关注分布式事务,以此确保数据的一致性。
另外,领域事件驱动也是微服务架构中的一个重要机制,它涵盖了微服务内和微服务之间的事件。
【微服务内】:通过事件总线(EventBus)来完成聚合之间的异步处理。
【微服务之间】:则是借助消息中间件来实现。这种异步化的领域事件驱动机制,属于一种间接的服务访问方式。当应用服务完成业务逻辑处理后,如果发生领域事件,就可以调用事件发布服务来完成事件发布。而当接收到订阅的主题数据时,事件订阅服务会调用事件处理领域服务,进而完成进一步的业务操作。
1.3 服务的封装与组合
我们看一下下面这张图。微服务的服务是从领域层逐级向上封装、组合和暴露的
【基础层】
基础层的主要服务形态为仓储服务,它包含接口和实现两部分。仓储接口服务供应用层或领域层服务调用,而仓储实现服务则负责完成领域对象的持久化或数据初始化。
【领域层】
领域层的核心任务是实现核心业务逻辑,负责表达领域模型的业务概念、业务状态和业务规则。其主要服务形态有实体方法和领域服务。
实体方法
:实体采用充血模型,在实体类内部实现与实体相关的所有业务逻辑,以实体类中的方法作为实现形式。实体是微服务的原子业务逻辑单元。在设计实体时,主要聚焦于实体自身的属性和业务行为,以此实现领域模型的核心基础能力。无需过多考虑外部操作和业务流程,从而确保领域模型的稳定性。DDD 提倡富领域模型,尽可能将业务逻辑归属到实体对象上。
领域服务
:对于那些实在无法归属到实体对象上的业务逻辑部分,则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,进而实现跨多个实体的复杂核心业务逻辑。在严格分层架构中,如果单个实体的方法需要向应用层暴露,那么必须通过领域服务封装后,才能提供给应用服务。
【应用层】
应用层主要用于表述应用和用户行为,承担着多项重要职责。
负责服务的组合、编排和转发;处理业务用例的执行顺序以及结果的拼装;协调不同聚合之间的服务和数据;负责微服务之间的事件发布和订阅。
通过应用服务对外暴露微服务的内部功能,如此便能隐藏领域层核心业务逻辑的复杂性以及内部实现机制。
应用层的主要服务形态包括应用服务、事件发布和订阅服务。应用服务内用于组合和编排的服务,主要来源于领域服务,也可能是外部微服务的应用服务。除完成服务的组合和编排外,应用服务还能实现安全认证、权限校验、初步的数据校验和分布式事务控制等功能。为实现微服务内聚合之间的解耦,聚合之间的服务调用和数据交互应通过应用服务来完成,原则上应禁止聚合之间的领域服务直接调用和聚合之间的数据表关联。
【用户接口层】
用户接口层是前端应用和微服务之间进行服务访问和数据交换的桥梁。
它负责处理前端发送的 Restful 请求,解析用户输入的配置文件等,并将数据传递给应用层。
在获取应用服务的数据后,进行数据组装,向前端提供数据服务。其主要服务形态是 Facade 服务,该服务分为接口和实现两个部分,主要完成服务定向,以及 DO 与 DTO 数据的转换和组装,实现前端与应用层数据的转换和交换。
1.4 两种分层架构的服务依赖关系
- 松散分层架构的服务依赖
我们看一下下面这张图,在松散分层架构中,领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。松散分层架构的服务依赖关系,无需逐级封装,可以快速暴露给上层。但它存在一些问题,第一个是容易暴露领域层核心业务的实现逻辑;第二个是当实体方法或领域服务发生服务变更时,由于服务同时被多层服务调用和组合,不容易找出哪些上层服务调用和组合了它,不方便通知到所有的服务调用方。
我们再来看一张图,在松散分层架构中,实体 A 的方法在应用层组合后,暴露给用户接口层 aFacade。abDomainService 领域服务直接越过应用层,暴露给用户接口层 abFacade 服务。松散分层架构中任意下层服务都可以暴露给上层服务。
- 严格分层架构的服务依赖
下面我们来了解一下严格分层架构中的服务调用规则。在严格分层架构体系里,每一层服务都有着明确的调用限制,即只能向紧邻的上一层提供服务。
以领域层为例,虽然实体、实体方法和领域服务都处于该层,但它们之间的调用有着严格的规定。实体和实体方法仅能暴露给领域服务,而领域服务则只能为应用服务提供服务。
在这种严格分层架构中,若服务存在跨层调用的需求,必须遵循特定的流程。也就是说,下层服务需要在上层进行封装后,才能够实现跨层服务的提供。例如,当实体方法需要为应用服务提供服务时,就必须先将其封装成领域服务。
这样做存在诸多好处。一方面,通过封装操作,可以有效避免核心业务逻辑的实现细节暴露给外部,从而增强了系统的安全性和稳定性。另一方面,将实体和方法封装成领域服务,能够防止应用层过度承载本应属于领域层的核心业务逻辑,避免应用层变得臃肿复杂,保证了各层职责的清晰划分。
此外,在服务发生变更的情况下,由于服务仅被紧邻的上层服务调用和组合,所以在处理变更时,只需逐级告知紧邻的上层服务即可。与松散分层架构相比,这种严格分层架构在服务的可管理性方面无疑具有明显的优势。
我们还是看图,A 实体方法需封装成领域服务 aDomainService 才能暴露给应用服务 aAppService。abDomainService 领域服务组合和封装 A 和 B 实体的方法后,暴露给应用服务 abAppService。
数据对象视图
在深入了解微服务架构时,数据对象是其中一个关键的组成部分。那么,微服务内究竟存在哪些类型的数据对象呢?这些数据对象之间又是如何进行协作与转换的呢?接下来,我们就一起探讨一番。
首先是数据持久化对象 PO(Persistent Object)。它与数据库结构呈现出一一对应的映射关系,在数据持久化的过程中,PO 充当着数据载体的重要角色。
其次是领域对象 DO(Domain Object)。它是微服务在运行时的实体,承载着微服务的核心业务内容,是业务逻辑实现的关键载体。
还有数据传输对象 DTO(Data Transfer Object)。DTO 主要用于前端与应用层之间,或者不同微服务之间的数据组装与传输工作,是应用之间进行数据传输的重要载体。
最后是视图对象 VO(View Object)。它的主要作用是对展示层指定页面或组件的数据进行封装,以满足前端展示的需求。
为了更清晰地理解这些数据对象,我们可以结合下面的这张图,进一步探究微服务各层数据对象的具体职责以及它们之间的转换过程。
原创 greencoatman 二进制跳动