一、从MVC到DDD
一、从MVC到DDD架构演进
1、MVC架构
1.1、MVC架构介绍
软件工程中很多问题都可以通过分层来解决,比如计算机缓存设计,分为一级二级三级等缓存层次,目的是为了解决内存和磁盘之间速度不匹配问题,通过引入分层的设计,将这种速度不匹配带来的影响降到了最低,再比如在数仓建模中,数据仓库通常分为ods,dwd,dwm,dws层,其实也是将复杂的问题简单化,引入分层的设计,让数据链路之间的依赖更加清楚,保证数据结构层次清洗,方便产出需要的数据,同理,今天要介绍的MVC软件架构模式,也是一种分层的思想。
面向过程的语言设计的网站系统,通常是方法之间的相互调用,比如A调用B,B调用C,方法之间相互调用完成一个业务流程,开发完的软件结构混乱不好维护,在面向对象开发以来,为了开发出的网站好维护,降低开发成本,逐步形成MVC软件架构,主要包括M-model对象层,主要封装一些实体对象,V-view展示层,通常用来向用户展示界面,但是目前大多都是前后端分离,所以这个V被淡化了很多,C-controller控制层,主要负责对外提供访问接口,DAO被抽象出来主要用来操作数据库。所以MVC-D这种软件架构使我们开发网站有了理论指导依据。
上图中mvc分层架构模式中,应用程序的各个层次功能明确,应用程序所需要的实体对象,方法,接口都被合理的分配在各个层次中,方便后续的迭代和开发维护,如果新增功能,只需要再不同层次中新增职责模块即可。
1.2、MVC架构调用流程
上图中就是MVC架构模式中,用户的一个请求在应用内部的链路情况,以用户发送http请求开始,Controller组件在收到请求后,调用spring容器中的Service方法,在Service对象中主要封装了业务方法,Service方法内部会注入DAO对象,负责和数据库交互,最终的结果在以相反的顺序返回到Controller层,最终返回给前端展示。
同时我们也可以看到各个对象在这些请求之间的作用,比如请求对象,库表映射对象以及响应结果对象。
2、DDD架构
2.1、DDD是什么
借用百科大的一段话,领域驱动设计(Domain-Driven Design,DDD)是一种软件架构设计方法,主要关注于解决复杂业务问题的软件系统。DDD 强调将业务领域知识融入到软件设计中,以便更好地理解和解决问题。这种方法通常用于大型软件系统的开发,特别是那些涉及多个团队和跨平台的系统。
注意:DDD本质不是一种软件架构,而是一种软件架构的设计方法,借助这种设计方法,可以很好的划分软件中各个模块的边界,设计中高内聚,低耦合的软件,方便后续的软件架构扩展。
DDD 的核心思想是将软件系统分解为多个子系统,每个子系统都负责处理特定的业务领域。这种分解方式使得软件系统更加模块化,易于维护和扩展。同时,DDD 强调将业务规则和逻辑与软件系统的实现细节分离,以便更好地管理和维护这些规则。
DDD 的核心概念包括实体(Entity)、值对象(Value Object)、聚合(Aggregate)、领域事件(Domain Event)和仓库(Repository)等。这些概念用于描述软件系统中的不同组件和关系,以便更好地理解和设计系统。
在大概了解了DDD是什么以及DDD的核心思想后,我们再来看下DDD是通过什么方法设计软件架构的。
2.2、DDD两阶段工程设计
在DDD软件架构设计中,有一套共识的两阶段设计方法,用来帮助DDD开发人员更好的划分软件界限,包括战略设计和战术设计,既然分为两个阶段设计软件系统,那我们首先需要明白两个阶段分别完成什么工作,交付什么样的成果。
战略设计
战略设计关注的是整个系统的宏观架构和核心领域的模型设计。在战略设计阶段,我们需要对整个系统进行规划,明确各个子域的职责和边界,以及它们之间的关系。战略设计的主要目标是建立一个清晰、一致的领域模型,为后续的开发工作提供指导和约束。
大白话就是在战略设计阶段,我们需要对软件系统的领域进行划分,比如交易与,营销域,账户域等等不同的领域,需要明确各个领域的职责和边界范围。那么在这个阶段,我们如何划分软件系统的领域边界呢?
答:首先需要对总体业务边界有了解,其一,通过抽象、分治的过程,合理的拆分为独立的多个微服务,从而分而治之。与之评价拆分的是否合理,则是在需求开发上线时候,是否每次都大量操作多个微服务开发和上线。这样的战略设计是一种失败的微服务单体设计。所以少数几个中等规模的单体应用,周围环绕着一个服务生态系统,这更有意义。其二,主要是对所熟知的业务,首先在业务层面对系统进行多个领域的划分,每一个领域的工作职责要明确,尽量让每一个领域的职责高度聚合,多个领域之间共享的内容越少越好,从业务层面划分出领域职责后,对每个业务进行单独建模抽象,这样在技术上就实现了单个领域的建模设计。
战术设计
战术设计则关注的是具体的实现细节和扩展性。在战术设计阶段,我们需要根据战略设计的指导,对各个子域进行深入的分析和设计,包括实体、值对象、聚合、仓库、服务等的设计。战术设计的目标是实现代码的可维护性、可扩展性和可测试性。
战略设计指导我们高层次的设计软件领域,战术设计则指导我们关注每一个领域的实现细节,因此在这个范畴下,主要以讨论如何基于面向对象思维,运用领域模型来表达业务概念。通常在不做领域模型设计的架构,也就是通常映射到 MVC 三层架构下,Service + 数据模型
的开发模式,会让 Service 扁平的、大量的,平铺出非常复杂的业务逻辑代码。再加上行为对象与功能逻辑的分离,贫血模型的开发方式,让行为对象的不断交叉使用,也是让系统不断增加复杂度,并到难以维护的根因。所以这一阶段要设计每一个可以表达领域概念的模型,并运用实体、聚合、领域服务来承载。
在实际应用中,战略设计和战术设计是相辅相成的。战略设计为整个系统提供了一个高层次的框架,为战术设计提供了指导和约束;而战术设计则是对战略设计的具体实现,通过深入分析业务需求和问题域,为系统提供更加精确和可靠的解决方案。
2.3、DDD分层架构
区别于MVC的设计思想,MVC通常设计通常以数据库为起点对业务系统进行建模设计,抽象出各种表结构实体,然后系统业务在表的基础上进行面向对象设计映射。而DDD则需要以业务领域模型为核心建模(即面向对象建模方式),更能体现对现实世界的抽象。故在DDD分层凸显领域层的重要作用,领域层为系统的核心,包括所有的业务领域模型的抽象表达,因此DDD和MVC在技术实现上,通过不同的分层设计,表现出两种设计方法的重点。
DDD软件层次划分:
DDD和MVC划分层次的区别
在架构设计上,在DDD分层结构中将传统三层架构的业务逻辑层拆解为应用层和领域层
其中Application划分为很薄的一层服务,非核心的逻辑放到此层去实现,核心的业务逻辑表现下沉到领域层去实现,凝练为更为精确的业务规则集合,通过领域对象去阐述说明。在建模方式上,DDD分层的建模思维方式有别于传统三层
传统三层通常是以数据库为起点进行数据库分析设计,而DDD则需要以业务领域模型为核心建模(即面向对象建模方式),更能体现对现实世界的抽象。
故在DDD分层凸显领域层的重要作用,领域层为系统的核心,包括所有的业务领域模型的抽象表达。在职责划分上,基础设施层涵盖了2方面内容
持久化功能,其中原三层架构的数据访问层下沉到基础设施层的持久化机制实现
通用技术支持,一些公共通用技术支持也放到基础设施层去实现。
接下来我们看一下相对于MVC模式的分层职责,DDD的分层每一层的工作职能:
2.3.1、用户接口层
用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。它处理前端发送的Restful 请求和解析用户输入的配置文件等,将数据传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。主要服务形态是 Facade 服务。Facade 服务分为接口和实现两个部分。完成服务定向,DO 与 DTO 数据的转换和组装,实现前端与应用层数据的转换和交换。
- 一般包括用户接口、Web 服务、rpc请求,mq消息等外部输入均被视为外部输入的请求。对外暴露API,具体形式不限于RPC、Rest API、消息等。
- 一般都很薄,提供必要的参数校验和异常捕获流程。
- 一般会提供VO或者DTO到Entity或者ValueObject的转换,用于前后端调用的适配,当然dto可以直接使用command和query,视情况而定。
- 用户接口层很重要,在于前后端调用的适配。若你的微服务要面向很多应用或渠道提供服务,而每个渠道的入参出参都不一样,你不太可能开发出太多应用服务,这样Facade接口就起很好的作用了,包括DO和DTO对象的组装和转换等。
没有负责业务逻辑,主要对外提供数据接口,理解为Controller层。
2.3.2、应用层
应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。除了同步方法调用外,还可以发布或者订阅领域事件,权限校验、事务控制,一个事务对应一个聚合根。
应用层负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。应用层的主要服务形态有:应用服务、事件发布和订阅服务。应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。
编排不同聚合实体之间的业务流程,封装复杂的业务,但是主要实现是下沉到domain层实现,应用层主要负责编排。
2.3.3、领域层
领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。
这里我要特别解释一下其中几个领域对象的关系,以便你在设计领域层的时候能更加清楚。首先,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,你要知道,实体和领域对象在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。
领域层主要的服务形态有实体方法和领域服务。实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力。不必过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。
- 包含了业务核心的领域模型:实体(聚合根+值对象),使用充血模型实现所有与之相关的业务功能,主要表达业务概念,业务状态信息以及业务规则。
- 真正的业务逻辑都在领域层编写,聚合根负责封装实现业务逻辑,对应用层暴露领域级别的服务接口。
- 聚合根不能直接操作其它聚合根,聚合根与聚合根之间只能通过聚合根ID引用;同限界上下文内的聚合之间的领域服务可直接调用;两个限界上下文的交互必须通过应用服务层抽离接口->适配层适配。
- 跨实体的状态变化,使用领域服务,领域服务不能直接修改实体的状态,只能调用实体的业务方法
DDD 提倡领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。对于严格分层架构,如果单个实体的方法需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务
实现主要的业务逻辑,实体,值对象,聚合,聚合根等都是在这一层实现。
2.3.4、基础层
也叫基础设施层,基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。
基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。
比如说,在传统架构设计中,由于上层应用对数据库的强耦合,很多公司在架构演进中最担忧的可能就是换数据库了,因为一旦更换数据库,就可能需要重写大部分的代码,这对应用来说是致命的。那采用依赖倒置的设计以后(说白了就是多套一层接口),应用层就可以通过解耦来保持独立的核心业务
- 为业务逻辑提供支撑能力,提供通用的技术能力,仓库写增删改查类似DAO。
- 防腐层实现(封装变化)用于业务检查和隔离第三方服务,内部try catch
仓储层实现以及PO持久化。
2.3.5、架构原则
在《实现领域驱动设计》一书中,DDD 分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。
而架构根据耦合的紧密程度又可以分为两种:严格分层架构和松散分层架构。优化后的DDD 分层架构模型就属于严格分层架构,任何层只能对位于其直接下方的层产生依赖。而传统的 DDD 分层架构则属于松散分层架构,它允许某层与其任意下方的层发生依赖。那我们怎么选呢?综合我的经验,为了服务的可管理,我建议你采用严格分层架构。
在严格分层架构中,领域服务只能被应用服务调用,而应用服务只能被用户接口层调用,服务是逐层对外封装或组合的,依赖关系清晰。而在松散分层架构中,领域服务可以同时被应用层或用户接口层调用,服务的依赖关系比较复杂且难管理,甚至容易使核心业务逻辑外泄。试想下,如果领域层中的某个服务发生了重大变更,那该如何通知所有调用方同步调整和升级呢?但在严格分层架构中,你只需要逐层通知上层服务就可以了。
DDD分层中的要素其实和三层分钟架构类似,只是在DDD分层架构中,这些要素被重新归类和划分了子层次,确定了层与层之间的交互规则和职责边界。
另外,三层架构数据访问采用DAO方式,DDD分层架构的数据库等基础资源访问,采用仓储(Repository)设计模式,通过依赖倒置实现各层对基础资源的解耦。
仓储服务又分为两部分:仓储接口和仓储实现,仓储接口放在领域层内,仓储实现放在基础层,原来三层架构中一些通用的第三方工具包,驱动,Common,Utility,Config等通用的公共资源统一放在基础层。
简单查询不涉及业务,是可以直接从应用层直接穿透到PO查询,不需要经过domain层。如下图所示,DDD本身是不限制非业务类操作跨层调用的。
DTO是不能存在于domain层的,DDD设计不认为DTO是业务对象,entity才是。或者传值简单数据类型也是可以的。
2.3.6、防腐层(ACL)
当某个功能模块需要依赖第三方系统提供的数据或者功能时,我们常用的策略就是直接使用外部系统的API、数据结构。这样存在的问题就是,因使用外部系统,而被外部系统的质量问题影响,从而“腐化”本身设计的问题。
因此我们的解决方案就是在两个系统之间加入一个中间层,隔离第三方系统的依赖,对第三方系统进行通讯转换和语义隔离,这个中间层,我们叫它防腐层:
两个系统之间加了中间层,中间层类似适配器模式,解决接口差异的对接,接口转换是单向的(即从调用方向被调用方进行接口转换);防腐层强调两个子系统语义解耦,接口转换是双向的。
在该架构中,上层模块可以调用下层模块,反之不行。即
- Interface ——> application | domain | infrastructure
- application ——> domain | infrastructure
- domain ——> infrastructure
因此,增加了防腐层之后,各个模块之间的调用关系如下:
DDD编码实践(改进分层)
在对上述的传统四层的实践中,
(1)根据依赖倒置原则对分层结构进行了改进,通过改变不同层的依赖关系(即将基础设施层倒置)来改进具体实现与抽象之间关系;
(2)在基础设施层中增加引用适配层(防腐层)来增强防御策略,用来统一封装外部系统接口的引用。
依赖倒置原则(DIP):
(1)高层模块不依赖于低层模块,两者都依赖于抽象;
(2)抽象不应该依赖于细节,细节应依赖抽象
2.4、服务间调用
图片来自网络
2.4.1、微服务内跨层服务调用
域内调用
微服务架构下往往采用前后端分离的设计模式,前端应用独立部署。前端应用调用发布在API 网关上的 Facade 服务,Facade 定向到应用服务。应用服务作为服务组织和编排者,它的服务调用有这样两种路径:
第一种是应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过仓储服务获取持久化数据对象,完成实体数据初始化。
第二种是应用服务直接调用仓储服务。这种方式主要针对像缓存、文件等类型的基础层数据访问。这类数据主要是查询操作,没有太多的领域逻辑,不经过领域层,不涉及数据库持久化对象。
注:域内调用不受任何限制
2.4.2、微服务之间的服务调用
微服务之间的应用服务可以直接访问,也可以通过 API 网关访问。由于跨微服务操作,在进行数据新增和修改操作时,你需关注分布式事务,保证数据的一致性。
跨域分为
1.同上下文跨域:ACL层->Adapter适配器层→调用其它域的repository。--->不得已才使用,不推荐使用。
推荐:1.使用领域事件 eventbus来做解耦
2.考虑是否有可能合并为一个领域.
2.跨上下文(肯定跨域):ACL层->Adapter适配器层->feign调用
2.4.3、领域事件驱动
领域事件驱动包括微服务内和微服务之间的事件。微服务内通过事件总线(EventBus)完成聚合之间的异步处理。微服务之间通过消息中间件完成。异步化的领域事件驱动机制是一种间接的服务访问方式。当应用服务业务逻辑处理完成后,如果发生领域事件,可调用事件发布服务,完成事件发布。当接收到订阅的主题数据时,事件订阅服务会调用事件处理领域服务,完成进一步的业务操作。
服务依赖
DDD 分层架构有一个重要的原则就是:每层只能与位于其下方的层发生耦合。
那根据耦合的紧密程度,分层架构可以分为两种:严格分层架构和松散分层架构。在严格分层架构中,任何层只能与位于其直接下方的层发生依赖。在松散分层架构中,任何层可以与其任意下方的层发生依赖。
在松散分层架构中,领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。松散分层架构的服务依赖关系,无需逐级封装,可以快速暴露给上层。
但它存在一些问题,第一个是容易暴露领域层核心业务的实现逻辑;第二个是当实体方法或领域服务发生服务变更时,由于服务同时被多层服务调用和组合,不容易找出哪些上层服务调用和组合了它,不方便通知到所有的服务调用方。
严格分层架构可以避免将核心业务逻辑的实现暴露给外部,将实体和方法封装成领域服务,也可以避免在应用层沉淀过多的本该属于领域层的核心业务逻辑,避免应用层变得臃肿。还有就是当服务发生变更时,由于服务只被紧邻上层的服务调用和组合,你只需要逐级告知紧邻上层就可以了,服务可管理性比松散分层架构要好是一定的。
2.5、DDD核心概念
- 值对象:value object 无唯一标识的简单对象
- 实体:entity 充血的领域模型,有唯一标识
- 聚合(聚合根):aggregate 实体的聚合,拥有聚合根,可为某一个实体
- 领域服务:service 无法归类到某个具体领域模型的行为
- 领域事件:event 不常用
- 仓储 repository:持久化相关,与基础设施层关联
- 工厂:factory 负责复杂对象创建
- 模块:module 子模块引入,可以理解为子域划分
2.5.1、充血模型和贫血模型
充血模型
Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了大多数相关的业务逻辑,也包含了依赖于持久层的业务逻辑, Business Logic层是很薄的一层,仅仅简单封装少量业务逻辑以及控制事务、权限逻辑等,不和DAO层打交道。所以,使用充血模型的领域对象是依赖于持久层的。代码架构层次结构是: Client-> Business Facade Service -> Business Logic Service -> Domain Object -> Data Access Service
比如如下user用户有改密码,改手机号,修改登录失败次数等操作,都内聚在这个user实体中,每个实体的业务都是清晰的,就是充血模型,充血模型的内存计算会多一些,内聚核心业务逻辑处理。
说白了就是,不只是有贫血模型中setter getter方法,还有其他的一些业务方法,这才是面向对象的本质,通过user实体就能看出有哪些业务存在。
优点
- 更加符合OO的原则
- Business Logic层很薄,符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑太过沉重,只充当Facade的角色,不和DAO打交道。
缺点
- 什么样的逻辑应该放在Domain Object中,什么样的业务逻辑应该放在Business Logic中,这是很含糊的。即使划分好了业务逻辑,由于分散在Business Logic和Domain Object层中,不能更好的分模块开发。熟悉业务逻辑的开发人员需要渗透到Domain Logic中去,而在Domian Logic又包含了持久化,对于开发者来说这十分混乱。
- 其次,因为Business Logic要控制事务并且为上层提供一个统一的服务调用入口点,它就必须把在Domain Logic里实现的业务逻辑全部重新包装一遍,完全属于重复劳动。
贫血模型
Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了对象的行为(例如:就像一个完整的人,具有一些属性如姓名、性别、年龄等,还具有一些能力,如走路、吃饭、恋爱等,这样才是一个完整的对象), 但不包含依赖Dao层(持久层)的业务逻辑。这部分依赖于Dao层的业务逻辑将会放到Business Logic层(业务逻辑层)中的服务类来实现,组合逻辑也由服务类负责。可以看出,贫血模型中的领域对象是不依赖于持久层的。代码架构层次结构是: Client-> Business Facade Service -> Business Logic Service(Business Logic Service是依赖Domain Object的行为) -> Data Access Service
直观点说即贫血模型具有一堆属性和set get方法,存在的问题就是通过pojo这个对象上看不出业务有哪些逻辑,一个pojo可能被多个模块调用,只能去上层各种各样的service来调用,这样以后当梳理这个实体有什么业务,只能一层一层去搜service,也就是贫血失忆症,不够面向对象。
优点
- 层次结构清楚,各层之间单向依赖
- 对于只有少量业务逻辑的应用来说,使用起来非常自然
- 开发迅速,易于理解
缺点
- 无法良好的应对非常复杂逻辑和场景
2.5.2、领域模型
在战略设计阶段,我们已经划分出软件系统的各个领域,因此领域模型是在各个子领域内,对领域进行建模设计,通常开发设计人员针对某一个具体领域内的业务规则,流程,进行抽象和封装,在设计手段上通过风暴模型拆分领域模块,形成界限上下文,本质是将MVC设计模型中Service层+数据模型拆分成拥有独立边界的领域模块,每个领域内可以创建自己的领域对象(实体,聚合,值对象),长处服务(DAO操作),工厂等。
领域
领域就是用来确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。简言之,DDD 的领域就是这个边界内要解决的业务问题域。
领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。领域可以拆分为多个子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问题拆分为小问题的过程。
在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。
决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。
这三类子域相较之下,核心域是最重要的,我们下面讲目的的时候还会以核心域为例详细介绍。通用域和支撑域如果对应到企业系统,举例来说的话,通用域则是你需要用到的通用系统,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。而支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等系统。
聚合根与领域服务负责封装实现业务逻辑。领域服务负责对聚合根进行调度和封装,同时可以对外提供各种形式的服务,对于不能直接通过聚合根完成的业务操作就需要通过领域服务。
说白了就是,聚合根本身无法完全处理这个逻辑,例如支付这个步骤,订单聚合不可能支付,所以在订单聚合上架一层领域服务,在领域服务中实现支付逻辑,然后应用服务调用领域服务。遵守以下规范:
- 同限界上下文内的聚合之间的领域服务可直接调用
- 两个限界上下文的交互必须通过应用服务层抽离接口->适配层适配。
2.5.3、限界上下文
我们可以将限界上下文拆解为两个词:限界和上下文。限界就是领域的边界,而上下文则是语义环境。通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流,简单来说限界上下文可以理解为语义环境。
综合一下,我认为限界上下文的定义就是:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。
在一个明媚的早晨,孩子起床问妈妈:“今天应该穿几件衣服呀?”妈妈回答:“能穿多少就穿多少!”那到底是穿多还是穿少呢?
如果没有具体的语义环境,还真不太好理解。但是,如果你已经知道了这句话的语义环境,比如是寒冬腊月或者是炎炎夏日,那理解这句话的涵义就会很容易了。
所以语言离不开它的语义环境。
而业务的通用语言就有它的业务边界,我们不大可能用一个简单的术语没有歧义地去描述一个复杂的业务领域。限界上下文就是用来细分领域,从而定义通用语言所在的边界。
正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。看到这,我想你应该非常清楚了,领域边界就是通过限界上下文来定义的。
理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。
可以说,限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
2.5.4、实体和值对象
- 实体
实体和值对象这两个概念都是领域模型中的领域对象。实体和值对象是组成领域模型的基础单元。
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。
在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态 实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。
而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息 customer 和账户信息 account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。
- 值对象
简单来说,值对象本质上就是一个集。那这个集合里面有什么呢?若干个用于描述目的、具有整体概念和不可修改的属性。那这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。
一个简单的例子:
人员的实体原本包括:姓名,年龄,性别以及人员所在的省,市,县,和街道等属性,但是这样显示地址等信息就显得非常零碎,因此,可以将地址的省,市,县,街道等属性提取出来单独构成一个地址属性集合,这个集合就是值对象。
值对象与实体一起构成聚合。值对象逻辑上是实体属性的一部分,用于描述实体的特征。值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。值对象是一些不会修改,只能完整替换的属性值的集合,你更关注他的属性和值,它没有太多的业务行为,用于描述实体的一些属性集,被实体引用,依附于实体的值对象基本没有自己的数据库表。是否要设计成值对象,你要看这个对象是否后续还会来回修改,会不会有生命周期。如果不可修改,并且以后也不会专门针对它进行查询或者统计,你就可以把它设计成值对象,如果不行,那就设计成实体吧。
在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
关于值对象,DDD引入值对象还有另一个重要原因,就是到底是领域建模有限还是数据库建模优先?
DDD提倡从领域建模设计触发,而不是先设计数据模型,传统的数据模型设计通常是一个表对应一个实体,是以主表关联多个从表结构,当实体表太多的时候就很容易陷入复杂的数据库设计中,领域驱动模型就很容易被数据模型绑架,因此值对象的诞生,在一定程度上,和实体是互补的。
同样的对象在不同的场景下,可能会设计出不同的结果。有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候你就可以将地址设计为值对象,比如收货地址。而在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。
2.5.5、聚合
实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。
聚合在其中起到什么样的作用?
例子:社会是有一个个的个体组成,象征这每一个人,随着社会的发展,慢慢出现了社团,机构,组织,部门等,我们开始从个人变成组织的一员,大家可以协同一致的工作,朝着更大的目标前进,发挥出更大的力量。
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合对象,它用来保证这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
你可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。
聚合根
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
下面采用保险业务例,看一下聚合构建过程包含哪些步骤:
2.5.6、领域事件
举例来说的话,领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可能是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发发送缴费邮件通知操作;或者一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。
在做用户旅程或者场景分析时,我们要捕捉业务、需求人员或领域专家口中的关键词:“如果发生……,则……”“当做完……的时候,请通知……”“发生……时,则……”等。在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。
领域事件相关案例
我来给你介绍一个保险承保业务过程中有关领域事件的案例。
一个保单的生成,经历了很多子域、业务状态变更和跨微服务业务数据的传递。这个过程会产生很多的领域事件,这些领域事件促成了保险业务数据、对象在不同的微服务和子域之间的流转和角色转换。在下面这张图中,我列出了几个关键流程,用来说明如何用领域事件驱动设计来驱动承保业务流程。
事件起点:客户购买保险 - 业务人员完成保单录入 - 生成投保单 - 启动缴费动作。
总之,通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同微服务之间的流转,实现微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。
一个完整的领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。
事件发布:构建一个事件,需要唯一标识,然后发布;
事件存储:发布事件前需要存储,因为接收后的事件也会存储,可用于重试或对账等;就是每次执行一次具体的操作时,把行为记录下来,执行持久化。
事件分发:服务内的应用服务或者领域服务直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等,支持同步或者异步。
事件处理:先将事件存储,然后再处理。
当然了,实际开发中事件存储和事件处理不是必须的。
因此实现方案:发布订阅模式,分为跨上下文(kafka,RocketMq)和上下文内(spring事件,Guava Event Bus)的领域事件。
2.7.7、事件风暴
事件风暴正是DDD战略设计中经常使用的一种方法,他可以快速分析和分解复杂的业务领域没完成领域建模。
参考:
2.6、数据对象视图
数据持久化对象 PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。最形象的理解就是一个 PO 就是数据库中的一条记录,好处是可以把一条记录作为一个对象处理,可以方便的转为其它对象。也有团队使用DO(Data Object)表示数据对象
领域对象 DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体,使用的是充血模型设计的对象。也有团队使用用 BO(Business Objects)表示业务对象的概念。
数据传输对象 DTO(Data Transfer Object):数据传输对象,主要用于远程调用之间传输的对象的地方。比如我们一张表有 100 个字段,那么对应的 PO 就有 100 个属性。但是客户端只需要 10 个字段,没有必要把整个 PO 对象传递到客户端,这时我们就可以用只有这 10 个属性的 DTO 来传递结果到客户端,这样也不会暴露服务端表结构。到达客户端以后,如果用这个对象来对应界面显示,那此时它的身份就转为 VO。DTO泛指用于展示层与服务层之间的数据传输对象,当然VO也相当于数据DTO的一种。
视图对象 VO(View Object):视图对象,主要对应界面显示的数据对象。对于一个WEB页面,小程序,微信公众号等前端需要的数据对象。也有团队用VO表示领域层中的Value Object值对象,这个要根据团队的规范来定义。
简单对象POJO(Plain Ordinary Java Object):简单对象,是只具有setter getter方法对象的统称。但是不要把对象名命名成 xxxPOJO!
我们结合下面这张图,看看微服务各层数据对象的职责和转换过程。
2.6.1、基础层
基础层的主要对象是 PO 对象。我们需要先建立 DO 和 PO 的映射关系。当 DO 数据需要持久化时,仓储服务会将 DO 转换为 PO 对象,完成数据库持久化操作。当 DO 数据需要初始化时,仓储服务从数据库获取数据形成 PO 对象,并将 PO 转换为 DO,完成数据初始化。大多数情况下 PO 和 DO 是一一对应的。但也有 DO 和 PO 多对多的情况,在 DO 和 PO数据转换时,需要进行数据重组
2.6.2、领域层
领域层的主要对象是 DO 对象。DO 是实体和值对象的数据和业务行为载体,承载着基础的核心业务逻辑。通过 DO 和 PO 转换,我们可以完成数据持久化和初始化。
2.6.3、应用层
应用层的主要对象是 DO 对象。如果需要调用其它微服务的应用服务,DO 会转换为DTO,完成跨微服务的数据组装和传输。用户接口层先完成 DTO 到 DO 的转换,然后应用服务接收 DO 进行业务处理。如果 DTO 与 DO 是一对多的关系,这时就需要进行 DO数据重组。
2.6.4、用户接口层
用户接口层会完成 DO 和 DTO 的互转,完成微服务与前端应用数据交互及转换。Facade服务会对多个 DO 对象进行组装,转换为 DTO 对象,向前端应用完成数据转换和传输。
2.6.5、前端应用
前端应用主要是 VO 对象。展现层使用 VO 进行界面展示,通过用户接口层与应用层采用DTO 对象进行数据交互。
DDD 基于各种考虑,有很多的设计原则,也用到了很多的设计模式。条条框框多了,很多人可能就会被束缚住,总是担心或犹豫这是不是原汁原味的 DDD。其实我们不必追求极致的 DDD,这样做反而会导致过度设计,增加开发复杂度和项目成本。DDD 的设计原则或模式,是考虑了很多具体场景或者前提的。有的是为了解耦,如仓储服务、边界以及分层,有的则是为了保证数据一致性,如聚合根管理等。在理解了这些设计原则的根本原因后,有些场景你就可以灵活把握设计方法了,你可以突破一些原则,不必受限于条条框框,大胆选择最合适的方法。
参考博文:
https://www.cnblogs.com/dennyzhangdd/p/14376904.html#_label2_0
https://blog.csdn.net/bookssea/article/details/127248954
https://blog.csdn.net/lqs_user/article/details/127799694
https://blog.csdn.net/u011537073/article/details/114267739
https://domain-driven-design.org/zh/ddd-concept-reference.html#模型驱动设计model-driven-design