微服务的设计和落地。
微服务落地时首先要确定的就是微服务的代码结构。
只有建立标准微服务代码模型和代码规范,才可以将领域对象所对应代码对象放在合适的软件包的目录结构中。
统一标准的代码模型的好处:
- 项目团队成员更好地理解代码,根据代码规范实现团队协作
- 微服务各层的逻辑互不干扰、分工协作、各据其位、各司其职,避免不必要的代码混淆
- 微服务架构演进时,轻松重构
DDD分层架构与微服务代码模型
参考DDD分层架构模型来设计微服务代码模型。没错!微服务代码模型就是依据DDD分层架构模型设计出来的。那为什么是DDD分层架构模型呢?
业务逻辑从领域层、应用层到用户接口层逐层封装和协作,对外提供灵活的服务,既实现了各层的分工,又实现了各层的协作。因此DDD分层架构模型就是设计微服务代码模型的最佳依据。
微服务代码模型
DDD并没有给出标准的代码模型,不同的人可能会有不同理解。
职责及目录结构
按DDD分层架构的分层职责定义。
在代码模型里分别为用户接口层、应用层、领域层和基础层,建立了 interfaces、application、domain 和 infrastructure 四个一级代码目录。
职能和代码形态
Interfaces(用户接口层)
主要存放用户接口层与前端交互、展现数据相关的代码。前端应用通过这层接口,向应用服务获取展现所需的数据。这层主要用来处理用户发送的Restful请求,解析用户输入的配置文件,并将数据传递给Application层。数据的组装、数据传输格式以及Facade接口等代码都会放在这一层目录里。
封装应用服务和对外暴露接口。
目录结构
assembler、dto 和 façade 三类
- assembler
实现DTO与领域对象之间的相互转换和数据交换。一般assembler与dto一同出现。比如创建用户时,将CustomerDTO转换为CustomerEntity - dto
数据传输的载体,内部不存在任何业务逻辑,可以通过DTO把内部的领域对象与外界隔离。比如接收请求传入的数据CustomerDTO。
不同的对象在不同的层转换。用户接口层DTO和DO转换,应用层主要是DO,调外部微服务的服务的时候应用层有dto和do的转换。领域层与基础层之间,在基础层有DO和PO的转换。
在接口层定义DTO对象。数据可能来源于多个DO对象。 - Facade
提供较粗粒度的调用接口,将用户请求委派给一个或多个应用服务进行处理。比如调用应用层创建用户的方法。
Application(应用层)
主要存放应用层服务组合和编排相关的代码。应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合,向上为用户接口层提供各种应用数据展现支持服务。应用服务和事件等代码会放在这一层目录里。
- Event(事件)
主要存放事件相关代码。包括两个子目录:- publish
主要存放事件发布相关代码。比如发布用户创建事件给其它微服务。 - subscribe
主要存放事件订阅相关代码(事件处理相关的核心业务逻辑在领域层实现)。
- publish
虽然应用层和领域层都可进行事件的发布和处理,但为实现事件的统一管理,推荐将微服务内所有事件的发布和订阅处理都统一放到应用层,事件相关的核心业务逻辑实现放在领域层。通过应用层调用领域层服务,来实现完整的事件发布和订阅处理流程。
- Service(应用服务)
应用服务会对多个领域服务或外部应用服务进行封装、编排和组合,对外提供粗粒度的服务。应用服务主要实现服务组合和编排,是一段独立的业务逻辑。
可以将所有应用服务放在一个应用服务类里,也可以把一个应用服务设计为一个应用服务类,以防应用服务类代码量过大。
比如内部服务->创建用户;外部服务->创建日志。
Domain(领域层)
主要存放领域层核心业务逻辑相关的代码。领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑。聚合以及聚合内的实体、方法、领域服务和事件等代码会放在这一层目录里。
Domain 由一或多个聚合包构成,共同实现领域模型的核心业务逻辑。
聚合内的代码模型是标准和统一的,包括:entity、event、repository 和 service 四个子目录。
Aggregate(聚合)
聚合软件包的根目录,可根据实际项目的聚合名称命名,比如权限聚合。在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。聚合内实现高内聚的业务逻辑,它的代码可以独立拆分为微服务。
以聚合为单位的代码放在一个包里的主要是为业务内聚,更是为以后微服务之间聚合的重组。聚合之间清晰的代码边界,可让你轻松地实现以聚合为单位的微服务重组。
示例
比如进入用户聚合目录下(如CustomerAggregate)。
假设这样一个场景,主播账户作为一个聚合,优惠券模块作为一个聚合。那主播选券的命令属于主播账户聚合。然后主播账户里的优惠券就是这个聚合里的值对象。
如果有多个聚合, 比如聚合根A和聚合根B, 从业务的角度讲,可以接受AB间数据的最终一致性,但从数据展示的角度考虑, A和B是有强关联性的,也就是说在页面上,他们总是一起在页面的某部分出现, 那可以分别调两个聚合的领域服务,然后将两个聚合根的DO对象转换为一个DTO,就可以给前端提供包含两个聚合数据的数据服务了。
Entity(实体)
存放聚合根、实体、值对象以及工厂模式(Factory,工厂模式主要是实现复杂聚合的实体的数据初始化。如果实体太多,聚合根处理起来会很复杂,通过工厂一次初始化)相关代码。实体类采用充血模型,同一实体相关的业务逻辑都在实体类代码中实现。跨实体的业务逻辑代码在领域服务中实现。比如用户聚合根。
-
Event(事件)
存放事件实体以及与事件活动相关的业务逻辑代码。比如创建用户的事件。 -
Service(领域服务)
存放领域服务代码。一个领域服务是多个实体组合出来的一段业务逻辑。你可以将聚合内所有领域服务都放在一个领域服务类中,你也可以把每一个领域服务设计为一个类。如果领域服务内的业务逻辑相对复杂,我建议你将一个领域服务设计为一个领域服务类,避免由于所有领域服务代码都放在一个领域服务类中,而出现代码臃肿的问题。领域服务封装多个实体或方法后向上层提供应用服务调用。比如具体的创建用户逻辑,比如用户是否重复校验,分配初始密码等。 -
Repository(仓储)
存放所在聚合的查询或持久化领域对象的代码,通常包括仓储接口和仓储实现方法。为了方便聚合的拆分和组合,设定原则:一个聚合对应一个仓储。比如将用户信息保存到数据库。
按DDD分层架构,仓储实现本应属基础层代码,但为在微服务架构演进时,保证代码拆分和重组的便利性,把聚合仓储实现的代码放到聚合包内。这样,如果需求或设计发生变化导致聚合需要拆分或重组,就可将包括核心业务逻辑和仓储代码的聚合包整体迁移,轻松实现微服务架构演进。
Infrastructure(基础层)
主要存放基础资源服务相关的代码,为其它各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。
Infrastructure 的代码目录结构有:config 和 util 两个子目录。
- Config
主要存放配置相关代码。 - Util
存放平台、开发框架、消息、数据库、缓存、文件、总线、网关、第三方类库、通用算法等基础代码,你可以为不同的资源类别建立不同的子目录。
总结
聚合之间的代码边界一定要清晰。聚合之间的服务调用和数据关联应该是尽可能的松耦合和低关联,聚合之间的服务调用应该通过上层的应用层组合实现调用,原则上不允许聚合之间直接调用领域服务。这种松耦合的代码关联,在以后业务发展和需求变更时,可以很方便地实现业务功能和聚合代码的重组,在微服务架构演进中将会起到非常重要的作用。
要有代码分层思想。
写代码时一定要搞清楚代码的职责,将它放在职责对应的代码目录内。
- 应用层代码主要完成服务组合和编排,以及聚合之间的协作,它是很薄的一层,不应该有核心领域逻辑代码
- 领域层是业务的核心,领域模型的核心逻辑代码一定要在领域层实现。如果将核心领域逻辑代码放到应用层,你的基于DDD分层架构模型的微服务慢慢就会演变成传统MVC架构。
在整个微服务架构里面一般微服务上层还有BFF层、聚合服务层,一般BFF层或聚合服务层用来协调多个微服务或者做数据转换。微服务内的应用层主要处理自己的逻辑编排,bff主要处理微服务之间的逻辑。
同一个微服务内,跨领域的方法调用,我们可以在应用层进行组合和编排,那微服务间的领域方法调用是怎样的呢?
从应用层发起。方法是逐层封装,一直到应用服务。微服务内应尽量避免领域服务在不同聚合之间的调用,这样聚合之间耦合度会比较高。
Controller, Service, Repository。Controller相当于用户接口层里的Facade。由于采用了充血模型,之前三层模型中的Service的业务逻辑被封装在了domain的各个聚合下的实体之中。如果需要使用到多个实体来完成某个操作,就要使用聚合中的service。
FAQ
-
应用服务只能调用领域服务和实体的方法,能调用仓储接口的方法么?按理应该隔离,即应用服务应该调用领域服务的方法,再让领域服务调用仓储接口的方法吧?
如果是应用服务直接调用文件或者缓存,应用服务可以之间调用仓储。但如果中间有领域实体和数据库,则需通过领域服务,然后通过聚合根来调用仓储。 -
实体的转换只有从用户接口层到应用服务层一次是么?即到应用服务层后,以及之后的仓储接口都是可以直接对领域实体进行操作的?
用户接口层大多是DTO,应用层和领域层大多是DO,基础层则是PO,在不同层之间是需要进行数据转换的。 -
需要在实体中配置一些和底层存储相关的注解,这样会不会不能把领域层可仓储实现进行隔离?如果这样,那Spring Data Jdbc是不是没有严格遵守DDD?而且它提供的领域事件的发布机制实现,是在对应的实体中产生,例如在某一实体中定义产生领域事件的源头,当对应的实体保存或更新时,就会发出这样一个领域事件。按照咱们文章中讲解的事件的发布是在应用层,那么如果要这样做的话,是不是就需要在应用层重新转发领域层实体内产生的领域事件呢?
如果是这样,确实领域层与数据库层会有耦合。领域事件其实放领域层也可,放应用层主要是为统一管理。如果领域事件放在实体内部,查找和运维起来就不是太方便,而且这个实体还需要对领域事件的实体进行操作。目录结构的设计主要是从边界、分层和便利性考虑的。