DDD让我充血了

发布于 2023-01-19  391 次阅读


服务到底怎么划分?服务的边界到底在哪里?适合的怎么才算适合?到底怎么设计?真让人摸不到头脑。

第一次上手搞这些东西令我犹犹豫豫,不敢下手。

领域模型和数据模型

领域模型一般是贴合业务的,数据模型一般是注重技术实现上的。

领域模型关注的是领域知识,是业务领域的核心实体,体现了问题域里面的关键概念,以及概念之间的联系。领域模型建模的关键是看模型能否显性化、清晰的表达业务语义,扩展性是其次。
数据模型关注的是数据存储,所有的业务都离不开数据,都离不开对数据的 CRUD,数据模型建模的决策因素主要是扩展性、性能等非功能属性,无需过分考虑业务语义的表征能力

通常我们的定义的实体中,一般不会带有行为,只是属性,一堆get/set,没有增删改查等操作,这些操作一般在service中。
领域模型中的实体是会有的,把领域对象看成一个实体,这个实体在各种状态变换后仍是保持一致的,而不是属性的变换。对这些对象而言,重要的不是其属性,而是其延续性和标识。

实体和值对象

定义:
实体:许多对象不是由它们的属性来定义,而是通过一系列的连续性(continuity)和标识(identity)来从根本上定义的。只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即使这些属性对系统用户非常重要),那它就是一个实体。
值对象:当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。

1674100798423.png

当然如何划分实体与值对象是根据业务来的。

实体人员,原包括:姓名、年龄、性别及所在省、市、县和街道等属性。这样显示地址相关属性就很零碎。
就可将 “省、市、县和街道等属性” 拿出来构成一个 “地址属性集合”,该集合就是值对象。领域模型会更加贴合面对对象编程,对象之间有明确的上下文关系。
在领域模型中人员是实体,地址是值对象,地址值对象被人员实体引用。在数据模型设计时,地址值对象可以作为一个属性集整体嵌入人员实体中,组合形成上图这样的数据模型。

 

贫血模型

在贫血模型中领域模型一般都是贫血模型。平常用的mvc架构基本上都是如此。

举个例子来讲,User、UserDAO 作为数据访问层,UserBO、UserService 作为业务逻辑层,UserVO、UserController 作为接口层;

其中 UserBO 只作为纯粹的数据结构,没有业务处理,业务逻辑集中在 Service 中。

像 UserBO 这样的纯数据结构的就可以称之为贫血模型,同样的还有 User 和 UserVO,这样的设计破坏了 Java 面向对象设计的封装特性,属于面向过程的编程风格。

充血模型

基于充血模型的 DDD 开发模式,与贫血模型相反的是,充血模型将数据和业务放在一个类里面。DDD 领域驱动设计,DDD 核心是为了根据业务对系统的服务进行拆分。领域驱动设计的核心还是基于对业务的理解,不能一味追求这样的概念。

对于充血模型的开发的 MVC 架构,其核心区别在于 Service 层:包含 Domain 类和 Service 类。Domain 对于 BO 而言,添加了一定的业务逻辑,降低 Service 中的业务逻辑量。那么充血模型对于贫血模型好在哪里呢?

对于贫血模型而言,由于数据和业务的分离,数据在脱离业务的情况下可以被任务程序修改,数据操作将不受限制等。

为什么贫血模型这么盛行?一是对于大部分业务而言都比较简单,基本上都是围绕 SQL 的 CRUD 操作,仅仅通过贫血模型设计就可以完成业务。而是充血模型的设计难度较大。

 

DDD分层架构

领域模型关注的是领域知识,是业务领域的核心实体,体现了问题域里面的关键概念,以及概念之间的联系。领域模型建模的关键是看模型能否显性化、清晰的表达业务语义,扩展性是其次。

数据模型关注的是数据存储,所有的业务都离不开数据,都离不开对数据的 CRUD,数据模型建模的决策因素主要是扩展性、性能等非功能属性,无需过分考虑业务语义的表征能力
1674099232815.png
1.用户接口层 UI,负责界面展示。
2.应用层Application Layer,负责业务流程
3.领域层Domain,负责领域逻辑。
4.基建层Infrastructure Layer,负责提供基建。
分类的依据是:越往上,预期变动越频繁;越往下,预期变动越少。

用户接口层

面向前端用户提供服务和数据适配。这一层聚集了对外接口和数据适配相关的功能。用户接口层在前后端分离设计时,主要完成后端微服务与前端不同用户的接口和数据适配。

用户接口层主要有Facade接口和DTO以及DO数据的组装和转换等代码逻辑。

应用层

应用层是用来连接用户接口和领域层的,很薄的一层,主要只能是协调领域层多个聚合完成服务的组合和编排,调度用的,不体现业务逻辑。

应用层之上是用户接口层,在应用层完成领域层服务组合和编排后,应用服务被用户接口层Facade服务封装,完成接口和数据适配后,以粗粒度的服务通过API网关面向前端应用发布。

此外,应用层也是微服务之间服务调用的通道,微服务在应用层可以调用其他微服务的应用服务,完成微服务之间的服务组合和编排。

在应用层主要有应用服务、事件订阅和发布等相关代码逻辑。

其中,应用服务主要负责服务的组合、编排和转发,处理业务用例的执行顺序以及结果的拼装。在应用服务中还可以进行安全认证、权限校验、事务控制、领域事件发布或订阅等。

注意:在微服务设计和开发时,应用层主要职能是服务的组合和编排,切记不要将本该在领域层的核心领域逻辑在应用层实现。这会使得领域模型失焦,时间一长应用层和领域层的边界就会变得混乱,边界清晰的四层架构慢慢可能就演变成了业务逻辑混杂的三层架构了。

领域层

领域层位于应用层之下,是领域模型的核心,主要实现领域模型的核心业务逻辑,体现领域模型的业务能力。用来表达业务概念、业务状态和业务规则,可以通过各种业务规则校验手段保证业务的正确性。

在设计时,领域层主要关注实现领域对象或者聚合自身的原子业务逻辑,不太关注外部用户操作或者流程等方面的业务逻辑。所以在领域层主要体现的是领域模型的能力。外部易变的如流程、业务组合和编排的需求由应用层完成。这样设计可以保证领域模型不易受外部需求的变化而受影响,从而保证领域模型的稳定。

领域建模时提取的大部分领域对象都放在领域层。微服务的领域层可能会有多个聚合,聚合内部一般都有聚合根、实体、值对象和领域服务等领域对象。它们组合在一起协同实现领域模型的核心业务能力。

注意:在选择用实体方法或者领域服务实现业务逻辑时,请记住不要滥用领域服务。如果将单一实体自身的业务行为也用领域服务来实现,这样就很容易变成贫血模型。

基础层

基础层贯穿了DDD所有层,它的主要职能就是为其他各层提供通用的技术和基础服务,包括如第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。我们常见的功能是完成实体的数据库持久化。

基础层主要有仓储服务代码逻辑。仓储采用依赖倒置设计,封装基础资源逻辑的服务实现,实现应用层、领域层与基础层的解耦,降低外部资源变化对领域逻辑的影响。

 

微服务代码目录结构
1674099896087.png

服务视图

1674102825374.png

基础层

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

领域层

领域层实现核心业务逻辑,负责表达领域模型业务概念、业务状态和业务规则。
领域层主要服务的形态有实体方法和领域服务。
实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,具体实现形式是实体类中的方法。实体是微服务内的原子业务对象,在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力,这是一种面向对象的编程方法。
实体方法不过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。
DDD提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。
你也可以认为领域服务是介于实体和应用服务之间的薄薄的一层。它的主要职能是实现领域层复杂核心领域逻辑的组合和封装。
采用严格分层架构时,实体方法如果需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务。

应用层

应用层主要面向前端应用和用户,根据前端用例和流程要求,通过服务组合和编排实现粗粒度的业务行为。
应用层主要服务形态有:应用服务和事件订阅服务。
应用服务负责服务的组合、编排和转发,负责处理业务用例的执行顺序和结果的拼装,负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。
通过应用服务对外暴露微服务的内部核心领域功能,这样可以隐藏领域层核心业务逻辑的复杂性和内部的实现机制。
应用服务用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。
除了完成服务的组合和编排外,应用服务内还可以完成安全认证、权限校验、初步的数据校验和分布式事务控制等功能。
提示:为了微服务内聚合的解耦,聚合之间的服务调用和数据交互,可通过应用服务完成。原则上我们应该尽量避免聚合之间的领域服务直接调用和聚合之间的数据库表关联。

用户接口层

用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。
用户接口层的主要服务形态是facade接口服务。
facade接口服务处理前端发送的Restful请求和解析用户输入的配置文件等,将数据传递给应用层。或者获取应用服务的数据后,进行数据组装,向前端提供数据服务。
facade接口服务分为接口和实现两个部分,完成服务定向。通过assembler组装器,完成DO与DTO数据的转换和组装,完成前端应用与应用层数据的转换和交换。
facade接口服务本质上就是端口适配器架构模型中的适配器,面向前端应用和用户提供主动适配。

数据视图

在DDD中有很多的实体和数据对象,这些对象分布在不同的层里。它们在不同的阶段有不同的形态,分别承担不同的职能。
数据持久化对象 (Persistent Object,PO),与数据库结构一一映射,它是数据持久化过程中的数据载体。
领域对象(Domain Object,DO),微服务运行时核心业务对象的载体,DO一般包括实体或值对象。
数据传输对象(Data Transfer Object,DTO),用于前端应用与微服务应用层或者微服务之间的数据组装和传输。是应用之间数据传输的载体。
视图对象(View Object,VO),用于封装展示层指定页面或组件的数据。
可以通过下图来具体了解微服务各层数据对象的职责和转换过程。
1674103399904.png

基础层

微服务基础层的主要数据对象是PO。在设计时,我们需要先建立DO和PO的映射关系。大多数情况下DO和PO是一一对应的。但也有DO和PO多对多的情况。在DO和PO数据转换时,需要进行数据重组。对于DO对象较多复杂的数据转换操作,你可以在聚合用工厂模式来实现。
当DO数据需要持久化时,先将DO转换为PO对象,由仓储实现服务完成数据库持久化操作。
当DO需要构建和数据初始化时,仓储实现服务先从数据库获取PO对象,将PO转换为DO后,完成DO数据构建和初始化。

领域层

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

应用层

应用层主要对象有DO对象,但也可能会有DTO对象。应用层在进行不同聚合的领域服务编排时,一般建议采用聚合根ID的引用方式,应尽量避免不同聚合之间的DO对象直接引用,避免聚合之间产生依赖。
在涉及跨微服务的应用服务调用时,在调用其他微服务的应用服务前,DO会被转换为DTO,完成跨微服务的DTO数据组装,因此会有DTO对象。
在前端调用后端应用服务时,用户接口层先完成DTO到DO的转换,然后DO作为应用服务的参数,传导到领域层完成业务逻辑处理。

用户接口层

用户接口层主要完成DO和DTO的互转,完成微服务与前端应用数据交互和转换。
facade接口服务在完成后端应用服务封装后,会对多个DO对象进行组装,转换为DTO对象,向前端应用完成数据转换和传输。
facade接口服务在接收到前端应用传入的DTO后,完成DTO向多个DO对象的转换,调用后端应用服务完成业务逻辑处理。

前端应用

前端应用主要是VO对象。展现层使用VO进行界面展示,通过用户接口层与应用层采用DTO对象进行数据交互。
提醒:数据转换主要目的是为了各层解耦,以保证领域模型的稳定,也是为了让微服务具有更强的扩展能力和适配能力。但每一次数据转换都是以性能作为代价,在设计时需要在性能和扩展能力之间找到平衡。


啦啦啦!