16 视图:如何实现服务和数据在微服务各层的协作?
你好,我是欧创新。
在DDD分层架构和微服务代码模型里,我们根据领域对象的属性和依赖关系,将领域对象进行分层,定义了与之对应的代码对象和代码目录结构。分层架构确定了微服务的总体架构,微服务内的主要对象有服务和实体等,它们一起协作完成业务逻辑。
那在运行过程中,这些服务和实体在微服务各层是如何协作的呢?今天我们就来解剖一下基于DDD分层架构的微服务,看看它的内部结构到底是什么样的。
服务的协作
1. 服务的类型
我们先来回顾一下分层架构中的服务。按照分层架构设计出来的微服务,其内部有Facade服务、应用服务、领域服务和基础服务。各层服务的主要功能和职责如下。
Facade服务:位于用户接口层,包括接口和实现两部分。用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将数据传递给应用层。或者在获取到应用层数据后,将DO组装成DTO,将数据传输到前端应用。
应用服务:位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果拼装,对外提供粗粒度的服务。
领域服务:位于领域层。领域服务封装核心的业务逻辑,实现需要多个实体协作的核心领域逻辑。它对多个实体或方法的业务逻辑进行组合或编排,或者在严格分层架构中对实体方法进行封装,以领域服务的方式供应用层调用。
基础服务:位于基础层。提供基础资源服务(比如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务应用逻辑的影响。基础服务主要为仓储服务,通过依赖倒置提供基础资源服务。领域服务和应用服务都可以调用仓储服务接口,通过仓储服务实现数据持久化。
2. 服务的调用
我们看一下下面这张图。微服务的服务调用包括三类主要场景:微服务内跨层服务调用,微服务之间服务调用和领域事件驱动。
微服务内跨层服务调用
微服务架构下往往采用前后端分离的设计模式,前端应用独立部署。前端应用调用发布在API网关上的Facade服务,Facade定向到应用服务。应用服务作为服务组织和编排者,它的服务调用有这样两种路径:
- 第一种是应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过仓储服务获取持久化数据对象,完成实体数据初始化。
- 第二种是应用服务直接调用仓储服务。这种方式主要针对像缓存、文件等类型的基础层数据访问。这类数据主要是查询操作,没有太多的领域逻辑,不经过领域层,不涉及数据库持久化对象。
微服务之间的服务调用
微服务之间的应用服务可以直接访问,也可以通过API网关访问。由于跨微服务操作,在进行数据新增和修改操作时,你需关注分布式事务,保证数据的一致性。
领域事件驱动
领域事件驱动包括微服务内和微服务之间的事件(详见 [第 06 讲])。微服务内通过事件总线(EventBus)完成聚合之间的异步处理。微服务之间通过消息中间件完成。异步化的领域事件驱动机制是一种间接的服务访问方式。
当应用服务业务逻辑处理完成后,如果发生领域事件,可调用事件发布服务,完成事件发布。
当接收到订阅的主题数据时,事件订阅服务会调用事件处理领域服务,完成进一步的业务操作。
3. 服务的封装与组合
我们看一下下面这张图。微服务的服务是从领域层逐级向上封装、组合和暴露的。
基础层
基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。
领域层
领域层实现核心业务逻辑,负责表达领域模型业务概念、业务状态和业务规则。主要的服务形态有实体方法和领域服务。
实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力。不必过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。
DDD提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。
对于严格分层架构,如果单个实体的方法需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务。
应用层
应用层用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。
通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。应用层的主要服务形态有:应用服务、事件发布和订阅服务。
应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。除了完成服务的组合和编排外,应用服务内还可以完成安全认证、权限校验、初步的数据校验和分布式事务控制等功能。
为了实现微服务内聚合之间的解耦,聚合之间的服务调用和数据交互应通过应用服务来完成。原则上我们应该禁止聚合之间的领域服务直接调用和聚合之间的数据表关联。
用户接口层
用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。它处理前端发送的Restful请求和解析用户输入的配置文件等,将数据传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。主要服务形态是Facade服务。
Facade服务分为接口和实现两个部分。完成服务定向,DO与DTO数据的转换和组装,实现前端与应用层数据的转换和交换。
4. 两种分层架构的服务依赖关系
现在我们回顾一下DDD分层架构,分层架构有一个重要的原则就是:每层只能与位于其下方的层发生耦合。
那根据耦合的紧密程度,分层架构可以分为两种:严格分层架构和松散分层架构。在严格分层架构中,任何层只能与位于其直接下方的层发生依赖。在松散分层架构中,任何层可以与其任意下方的层发生依赖。
下面我们来详细分析和比较一下这两种分层架构。
松散分层架构的服务依赖
我们看一下下面这张图,在松散分层架构中,领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。松散分层架构的服务依赖关系,无需逐级封装,可以快速暴露给上层。
但它存在一些问题,第一个是容易暴露领域层核心业务的实现逻辑;第二个是当实体方法或领域服务发生服务变更时,由于服务同时被多层服务调用和组合,不容易找出哪些上层服务调用和组合了它,不方便通知到所有的服务调用方。
我们再来看一张图,在松散分层架构中,实体A的方法在应用层组合后,暴露给用户接口层aFacade。abDomainService领域服务直接越过应用层,暴露给用户接口层abFacade服务。松散分层架构中任意下层服务都可以暴露给上层服务。
严格分层架构的服务依赖
我们看一下下面这张图,在严格分层架构中,每一层服务只能向紧邻的上一层提供服务。虽然实体、实体方法和领域服务都在领域层,但实体和实体方法只能暴露给领域服务,领域服务只能暴露给应用服务。
在严格分层架构中,服务如果需要跨层调用,下层服务需要在上层封装后,才可以提供跨层服务。比如实体方法需要向应用服务提供服务,它需要封装成领域服务。
这是因为通过封装你可以避免将核心业务逻辑的实现暴露给外部,将实体和方法封装成领域服务,也可以避免在应用层沉淀过多的本该属于领域层的核心业务逻辑,避免应用层变得臃肿。还有就是当服务发生变更时,由于服务只被紧邻上层的服务调用和组合,你只需要逐级告知紧邻上层就可以了,服务可管理性比松散分层架构要好是一定的。
我们还是看图,A实体方法需封装成领域服务aDomainService才能暴露给应用服务aAppService。abDomainService领域服务组合和封装A和B实体的方法后,暴露给应用服务abAppService。
数据对象视图
在DDD中有很多的数据对象,这些对象分布在不同的层里。它们在不同的阶段有不同的形态。你可以再回顾一下 [第 04 讲],这一讲有详细的讲解。
我们先来看一下微服务内有哪些类型的数据对象?它们是如何协作和转换的?
- 数据持久化对象PO(Persistent Object),与数据库结构一一映射,是数据持久化过程中的数据载体。
- 领域对象DO(Domain Object),微服务运行时的实体,是核心业务的载体。
- 数据传输对象DTO(Data Transfer Object),用于前端与应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体。
- 视图对象VO(View Object),用于封装展示层指定页面或组件的数据。
我们结合下面这张图,看看微服务各层数据对象的职责和转换过程。
基础层
基础层的主要对象是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对象进行数据交互。
总结
今天我们分析了DDD分层架构下微服务的服务和数据的协作关系。为了实现聚合之间以及微服务各层之间的解耦,我们在每层定义了不同职责的服务和数据对象。在软件开发过程中,我们需要严格遵守各层服务和数据的职责要求,各据其位,各司其职。这样才能保证核心领域模型的稳定,同时也可以灵活应对外部需求的快速变化。
思考题
你知道在微服务内为什么要设计不同的服务和不同的数据对象吗?它体现的是一种什么样的设计思想?
欢迎留言和我分享你的思考,你也可以把今天所学分享给身边的朋友,邀请他加入探讨,共同进步。
- 瓜瓜 👍(20) 💬(2)
各种数据对象的转换放在那一层,很重要,比如vo与dto的转换放在前端应用,dto与do的转换放在用户接口层或者是应用层(根据用户接口层与应用层发生调用,还是微服务之间应用层发生调用而定),领域层只有DO,DO与PO的转换放在仓储实现里面,基础层只操作PO,至于仓储层的实现是放在领域层还是基础层,可以根据具体情况而定,放在基础层则为严格分层,放在领域层,则方便微服务的拆分和组合。望老师指正
2019-11-20 - 。 👍(15) 💬(9)
欧老师你好 用户接口层:入参是DTO,内部将DTO转化为DO后调用应用层,将应用层的结果转化为VO后返回给前台 应用层:入参是DO,返回值是DO 领域层:入参是DO,返回值是DO 基础层:入参是DO,内部将DO转化成PO进行数据库的增删改查,执行结果用PO去映射,再转化为DO作为基础层的返回值 问题1:时间范围查询时,会有辅助字段,如:beginTime和endTime,PO这怎么处理?我们的处理方式是增删改用PO,查询时候用QueryPO,QueryPO继承了PO并额外增加用于查询的辅助字段(比如时间、集合、模糊查询等),这样可以么? 问题2:有的查询功能,比如按照名称查询,查询条件就是name,DTO、DO和PO是一样的,也需要在每一层都去转化一下么?我们把查询时的对象命名为QueryPO,从用户接口层到基础层的入参都是这一个,这样可以么?
2019-11-20 - zj 👍(14) 💬(8)
应用层其实我觉得入参数是DTO比较好,因为应用层是要暴漏给其他微服务调用的。然后在应用层将DTO转为DO来调用领域服务。如果调用其他微服务,则构造对方服务需要的DTO来调用。
2019-11-29 - 胖大蟲 👍(7) 💬(6)
用户接口层会完成 DO 和 DTO 的互转,那不就等于将DO暴露给用户接口层了?按我的理解,DO从领域服务出来的时候就应该转换为DTO给应用层,从应用层开始往上(包括应用层)都不知道DO的存在;DO和DTO的互转,由领域服务负责,接收上层(应用层)传递的DTO,转换为DO,进而调用DO的方法完成业务逻辑,再将需要返回的数据转换为DTO返回给上层
2019-12-25 - hunter 👍(7) 💬(7)
传入数据的格式校验放在哪层做?例如手机号格式校验、姓名长度校验等
2019-11-20 - 阿信 👍(5) 💬(1)
关于数据对象视图定义,这块的想法和老师稍微有点区别 我的想法: https://www.processon.com/view/link/5e85bc85e4b0412013f87eb6 应用层对外是DTO,DO层不暴露到Facade层。
2020-04-20 - lamthun 👍(4) 💬(3)
老师, 你好, 按照上图中的服务调用与数据组合的思路. 看图示领域层只关心repository, 不关心缓存, 缓存还是由业务层进行封装, 是这样吗? 如果是这样子的话, 在大部分应用系统中, 领域层会不会又变成薄薄的增删查改这样的一层.
2020-05-26 - okjesse 👍(4) 💬(4)
请问应用层需要访问repository层返回一些查询数据时,repository是只能返回DO,还是说也可以返回为DTO呢,谢谢。
2019-12-19 - iMARS 👍(3) 💬(1)
看完这一节有一个感觉,如果系统的业务不复杂,或者属于从0到1发展阶段的,DDD的设计方式会拖慢开发的速度,增加系统的复杂度,不适合用DDD的方式。仅仅是VO-DTO-DO-PO之间的转换就存在效能的损耗,并增加了开发工作量。而对于业务复杂,又需要规模化弹性扩展的,需要引入DDD的方式对已有系统采用自下而上的方式进行重构,以便做到业务敏捷。
2020-10-10 - 珅珅君 👍(3) 💬(2)
如果需要依赖第三方的接口,应该放在哪,领域服务还是应用服务
2020-06-04 - 墨名次 👍(3) 💬(1)
在数据试图这里,如果有用户User,那么在后端代码中是不是会有: com.xxx.xxx.po.User com.xxx.xxx.do.User com.xxx.xxx.dto.User 或者为了方便区分则可以: com.xxx.UserPO com.xxx.User com.xxx.UserDTO ?
2019-11-20 - 小孩 👍(2) 💬(3)
没太看懂这里vo跟dto区别,我的用法一般是前段传vo过来转换成do处理然后po持久化,如果中间需要模型转换一个中间过程会创建dto
2020-08-13 - 发飙的蜗牛 👍(2) 💬(1)
老师,仓储服务返回值应该是DO还是PO呢?如果是PO,那么实体方法去调用就要自己去将PO转为DO,如果是DO的话就需要在仓储实现里面转,但是像spring data JPA这些框架,实现我们是不用去管的,我们只需要泛型化将PO传进去就可以了,但是只能返回PO 另一个问题想问下,对于DDD封层架构,事务控制应该放到那一层去做呢?如果不是放到一层,应该怎么去设计事务控制?
2020-02-29 - 梦终结 👍(2) 💬(2)
老师你好,我想问下: 1、DO里面是充血模型是么? 2、如果要是充血模型,那对DO的最基础的增删改查都写在DO里面是么?
2020-01-10 - 胡杨 👍(2) 💬(2)
领域实体是entity,领域对象是do,那Do就是entity么? 是的话,那po到do的转换一般是EF等框架自己做的了吧?
2019-12-13