前言
现一般都是使用UML面向对象建模,但是这种建模有种问题,就是需要对设计模式比较了解,且能熟练应用。不然在实践过程中大概率会把面向对象编程变成面向过程编程。耦合性高、灵活性差、重构难度也大。
为什么要使用领域驱动设计?
而 DDD本身就是理论的集合,领域驱动设计(DDD)提出是从系统的分析到软件建模的一套方法论。将业务概念和业务规则转换成软件系统中的概念和规则,从而降低或隐藏业务复杂性,使系统具有更好的扩展性,以应对复杂多变的现实业务问题。
总结它是一套完整而系统的设计方法、是一种设计思维、一种方法论,并不是"系统架构",一种架构设计原则、思维。
领域驱动设计的应用场景是什么?
- 适用于高复杂业务的产品研发、用于提炼稳定的产品内核(领域模型中称为核心域)
- 通过建模可提高建模高内聚、降低模型间的耦合度,提高系统的可扩展性与稳定性;
- 强调团队与领域专家的合作沟通,有助于建立一个沟通良好的团队组织;
- 统一设计思想与设计规范,有助于提高团队成员的架构设计能力和面向对象设计能力;
- 现有的微服务建构都是遵循领域驱动设计的架构原则;
现行对比
微服务架构:控制层->逻辑层->数据层
DDD架构:控制层->逻辑层->领域能力层->数据层。
名词解释
1、领域
领域指的是特定的业务问题领域,是专门用来确定业务的边界。
2、子域
而当一个业务领域比较复杂,会被分为多个子域,子域的类型:核心子域、通用子域、支撑子域:
1)核心子域:
业务成功的核心竞争力。简单来说就是领域中最重要的子域,如果没有它其他的都不成立,比如用户服务这个领域中的用户子域
2)通用子域:
不是核心,但被整个业务系统所使用。在领域这个层面中,这里指的是通用能力,比如通用工具,通用的数据字典、枚举这类(感叹DDD简直恨不得无孔不入)。在整个业务系统这个更高层面上,也会有通用域的存在,指的通用的服务(用户服务、权限服务这类公共服务可以作为通用域)。
3)支撑子域:
不是核心,不被整个系统使用,完成业务的必要能力。
3、核心域/支撑域/通用域
意思同上,唯一的区别但就是这些领域
4、通用语言/限界上下文
1)通用语言
指的是一个领域内,同一个名词必须是同一个意思,即统一交流的术语。比如我们在搞用户中心的时候,用户统一指的就是系统用户,而不能用其他名词来表达,目的是提高沟通的效率以及增加设计的可读性
2)限界上下文
限界上下文指的是领域的边界。 每个领域都会有边界,每个域可以在内部形成一个独立自洽的上下文环境,这就是限界上下文。本质来说,限界上下文是一种约定。
3)上下文映射图
用图的方式,表达出限界上下文之间关联。如 合作关系、防腐层等;
4)Repository
**Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。**简单来说,Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。
在设计Repository时,我们应该采用面向集合的方式,而不是面向数据访问的方式。这有助于你将自己的领域当作模型来看待,而不是 CRUD 操作;Repository是面向领域的,Repository定义的目的不是DB驱动的,Repository管理的数据的最小粒度是聚合根,这两点和DAO有很大不同。
5、领域划分规则
1)头脑风暴
简单来说就是一堆人坐在一个小会议室中开会,去梳理业务系统都有哪些业务事件。
2)事件风暴
指的是领域内的业务事件,比如用户中心中,新增用户,授权,用户修改密码等业务事件。
3)领域事件
**领域内,子域和子域之间交互的事件。**如用户服务中用户和角色交互是为用户分配角色,或者是为角色批量绑定用户,这里的领域事件有两个,一个是“为用户分配角色”,另一个是“为角色批量绑定用户”。
6、实体/值对象
1)实体
可以理解为有着唯一标识符的东西,比如用户实体(某个类)。
2)值对象
实体的具体化,比如用户实体中的T。实体和值对象可以简单的理解成Java中类和对象,只不过这里通常需要对应数据实体。值对象是没有唯一的标识符,通过其属性值来区分不同的对象。值对象通常被用于聚合内部,作为实体的属性或者组成聚合根的一部分。
具有以下特点:
- 不可变性:值对象的属性值在创建后不可改变,任何修改都应该创建一个新的值对象。
- 相等性:值对象的相等性根据其属性值来决定,相同属性值的值对象被认为是相等的。
- 无副作用:值对象的行为不会产生副作用,对其的操作不会改变系统的状态。
值对象的作用:
- 封装重复的属性,提高代码可读性和可维护性。
- 提供了一种更加表达领域概念的方式,增强了代码的语义性。
- 作为实体的属性,帮助实体建立复杂的关联关系。
- 支持领域行为的建模和封装。
7、聚合/聚合根
聚合
实体和实体之间需要共同协作来让业务运转,比如授权就是给用户分配一个角色,这里涉及到了用户和角色两个实体,这个聚合即是用户和角色的关系。
聚合根
聚合根是聚合的管理者,即一个聚合中必定是有个聚合根的,通常它也是对外的接口。
例如:
1)当业务场景是给用户分配某个角色,这个事件中涉及两个实体分别是用户和角色,这个时候用户就是聚合根。
**2)当业务场景是给角色批量绑定用户,那么聚合根就变成了角色。**即访问聚合必须通过聚合根来访问,这个也就是聚合根的作用。
8、贫血模型、充血模型
贫血模型
定义对象的简单的属性值,在属性值上直接get、set。简单来说,就是业务逻辑主要放在服务对象中,而领域对象(实体、值对象等)只具备数据和基本操作,缺乏自身的行为。这种模型会导致业务逻辑分散和难以维护。
充血模型
**充血模型就是在定义属性的同时也会定义方法,而要获取属性值是通过值对象来获取。**简单来说,指将业务逻辑尽可能地放在领域对象中,使其具备自身的行为和状态。这种模型可以更好地保护业务规则,提高内聚性和可维护性。
9、领域服务
领域服务是领域模型中的一种行为抽象,它表示一组操作或行为的集合,通常与领域对象无关。服务可以是无状态的,也可以是有状态的,它们通过接口或者静态方法来提供服务。
具有以下特点:
- 封装行为:服务封装了一组操作或者行为,在方法级别上对领域操作进行了组织和归纳。
- 强调整合:服务跨越多个领域对象,协调它们之间的交互,促进领域模型的整体性和一致性。
- 面向操作:服务的主要目的是执行某个操作,并且不保留任何状态。
服务的作用:
- 处理复杂的领域操作,协调多个实体或值对象之间的交互。
- 提供领域无关的功能,例如验证、计算等。
- 支持领域模型的完整性和一致性。
DDD 设计思想
领域驱动设计的思维是:对象+行为+服务,所有的设计围围绕着对象、行为、服务展开。
1、方案对比
1)传统的方案
三层应用架构:数据-应用(业务逻辑层)-展现,通常是以数据位为起点进行数据库分析设计。
缺点:
- 服务层过重,数据模型失血,没东西;
- 面条式编程或者面向数据库编程,服务层围绕数据库作业完成业务逻辑,经常一条线撸到底;
- 代码一整块一整块的过重,很难扩展复用;
- 数据库模型只是数据库映射,没有相关的行为支撑,行为都被上一层Service给完成等了,因此是失血 的领域模型;
2)领域驱动设计方案
三层应用架构,在DDD分层结构中将三层中业务逻辑拆解为应用层和领域层,核心业务逻辑表现下沉到领域层去实现,以业务领域模型为核心建模(面向对象建模)。
优点:
- 领域模型封装实现各自应有的行为,可以认为是一个高内聚、低耦合的组件,若是有新的行为,方便增加,不影响其他领域;
- 代码复用性高,业务逻辑清晰明确;
- 可以把用户的行为转换为领域事件,并进行智能服务编排。
一个领域是由一个或多个模型组成,从定义上讲模型是领域的抽象,从理解上将模型可以认为是一个高内聚、低耦合的组件、模块。也就是说领域模型重在建模过程,建模过程会抽象出一系列领域对象和领域服务。
2、战略建模
战略建模关注点在于系统物理划分,根据你对领域的分割结果及公司或部门的组织结构决策如何划分子服务,比如分出几个,服务之间如何交互,该层面往往暂不会涉及过多技术细节。
3、战术设计
战术设计依赖于领域模型和通用预言,通过技术模式将领域模型和通用预言中的概念映射到代码实现中。随着模型的进化,代码实现也会进行重构,以更好的体现模型概念。
如图所示:
知悉:
1、DDD是解决大型复杂项目的,比较简单的业务,不适合DDD。
2、DDD要有一个完整的、符合DDD原则的代码结构,这可能增加代码的复杂度,有可能导致项目进度失控。
3、DDD是一种框架,应该包含聚合根、实体、领域事件、仓储定义、限界上下文等一切DDD所倡导的元素;否则就不是DDD。
4、DDD需要大家严格遵循各自模块的边界,且存在着过多因为解耦带来的看似冗余没用的代码,会降低编码效率,造成“类膨胀”。
小结
说白了,领域驱动设计强调以"领域"为核心驱动力,通过模型驱动设计来保障领域模型与程序设计的一致,领域模型不应该包含任何技术实现因素,模型中的对象真实的表达了领域概念,却不受技术实现的约束,领域模型本身和技术无关,领域驱动从设计上划分为战略设计和战术设计。
当然DDD的开发模式也就是充分的遵循OOP 三大特性,封装,继承,多态。通过设计模式,和业务逻辑细分进行解决。
DDD架构设计
DDD是为了解决了这个业务边界的问题,是一种架构模式,也是一种划分业务领域范围的方法论。常见的领域驱动设计架构有经典的三层架构、REST架构、事件驱动架构、CQRS架构、六边形架构等。领域驱动设计是一种由域模型来驱动着系统设计的思想。不是通过存储数据词典(DB表字段、ES Mapper字段等等)来驱动系统设计。领域模型是对业务模型的抽象,DDD是把业务模型翻译成系统架构设计的一种方式。
**DDD 强调领域模型和微服务设计的一体性,先有领域模型然后才有微服务,而不是脱离领域模型来谈微服务设计。所谓的领域模型,实际上就是将一个业务按照DDD模型拆分,呈现层次化、结构化。模型拆分的最小粒度就是值对象、实体。**从技术角度看,最小粒度就是某张数据库表或字段,从产品角度看,就是某个最小的功能细节。
思考
当面向业务开发的过程中,首先应该思考的是领域模型而不是如何建表。而在大部分眼中其实就是CRUD,其根源在于表驱动设计思想而非领域驱动设计。
1、建模领域事件
**在建模领域事件时,应该根据限界上下文中的通用语言来命名事件及属性。如果事件由聚合上的命令操作产生,那么通常根据该操作方法的名字来命名领域事件。**事件的名字表明了聚合上的命令方法在执行成功之后所发生的事情,换句话说待定项以及不确定的状态是不能作为领域事件的。
在建模之前最好是画出当前业务的状态流转图,包含前置操作以及引起的状态变更,这里表达的是已经变更完成的状态所以我们不用过去时态表示,比如删除或者取消,即代表已经删除或者已经取消。然后对于其中的节点进行事件建模。
如下图:是文件云端存储的业务,分别对预上传、上传完成确认、删除等环节建模“过去时”事件,PreUploadedEvent、ConfirmUploadedEvent、RemovedEvent。
1)领域事件需要包含唯一ID、创建时间
主要用于事件追溯和日志。而如果是数据库存储,ID通常为唯一索引。创建时间用于追溯,事件存储都有可能遇到事件延迟,可以通过创建时间能够确保其发生顺序。
2)领域事件应该携带与事件发生时相关的上下文数据信息
领域事件应该携带与事件发生时相关的上下文数据信息,但是并不是整个聚合根的状态数据。
例如:在创建订单时可以携带订单的基本信息,而对于用户更新订单收货地址事件AddressUpdatedEvent事件,只需要包含订单、用户以及新的地址等信息即可。
//用户更新订单收货地址事件
public class AddressUpdatedEvent extends DomainEvent {
//通过userId+orderId来校验订单的合法性;
private String userId;
private String orderId;
//新的地址
private Address address;
//略去具体业务逻辑
}
2、领域事件的存储
领域事件的不可变性与可追溯性都决定了其必须要持久化的原则。
1)单独的EventStore
有的业务场景中会创建一个单独的事件存储中心,可能是Mysql、Redis、Mongo、甚至文件存储等。这里以Mysql举例,business_code、event_code用来区分不同业务的不同事件,具体的命名规则可以根据实际需要。实践中尽量避免使用分布式事务,或者尽量避免其跨库的场景,否则你就得想想如何补偿了。
// 考虑是否需要分表,事件存储建议逻辑简单
CREATE TABLE `event_store` (
`event_id` int(11) NOT NULL auto increment,
`event_code` varchar(32) NOT NULL,
`event_name` varchar(64) NOT NULL,
`event_body` varchar(4096) NOT NULL,
`occurred_on` datetime NOT NULL,
`business_code` varchar(128) NOT NULL,
UNIQUE KEY (`event id`)
) ENGINE=InnoDB COMMENT '事件存储表';
2)与业务数据一起存储
在分布式架构中,每个模块都做的相对比较小,准确的说是“自治”。如果当前业务数据量较小,可以将事件与业务数据一起存储,用相关标识区分是真实的业务数据还是事件记录;或者在当前业务数据库中建立该业务自己的事件存储,但是要考虑到事件存储的量级必然大于真实的业务数据,考虑是否需要分表。
这种方案的优势:数据自治;避免分布式事务;不需要额外的事件存储中心。当然其劣势就是不能复用。
3、领域事件的发布
1)由领域聚合发送领域事件
/*
* 一个关于比赛的充血模型例子
* 贫血模型会构造一个MatchService,我们这里通过模型来触发相应的事件
* 本例中略去了具体的业务细节
*/
public class Match {
public void start() {
//构造Event....
MatchEvent matchStartedEvent = new MatchStartedEvent();
//略去具体业务逻辑
DefaultDomainEventBus.publish(matchStartedEvent);
}
public void finish() {
//构造Event....
MatchEvent matchFinishedEvent = new MatchFinishedEvent();
//略去具体业务逻辑
DefaultDomainEventBus.publish(matchFinishedEvent);
}
//略去Match对象基本属性
}
2)事件总线VS消息中间件
**微服务内的领域事件可以通过事件总线或利用应用服务实现不同聚合之间的业务协同。**即微服务内发生领域事件时,由于大部分事件的集成发生在同一个线程内,不一定需要引入消息中间件。
但一个事件如果同时更新多个聚合数据,按照 DDD“一个事务只更新一个聚合根”的原则,可以考虑引入消息中间件,通过异步化的方式,对微服务内不同的聚合根采用不同的事务
3、Saga事务模式
Saga通过使用异步消息来协调一系列本地事务,从而维护多个服务之间的数据一致性。一个 Saga 表示需要更新的多个服务中的一个,即Saga由一连串的本地事务组成。每一个本地事务负责更新它所在服务的私有数据库,这些操作仍旧依赖于我们所熟悉的ACID事务框架和函数库。
Saga与TCC相比少了一步Try的操作,TCC无论最终事务成功失败都需要与事务参与方交互两次。而Saga在事务成功的情况下只需要与事务参与方交互一次, 如果事务失败,需要额外进行补偿回滚。
1)Saga实现
当通过系统命令启动Saga时,协调逻辑必须选择并通知第一个Saga参与方执行本地事务。一旦该事务完成,Saga协调选择并调用下一个Saga参与方。这个过程一直持续到Saga执行完所有步骤。如果任何本地事务失败,则 Saga必须以相反的顺序执行补偿事务。
1、协同式
把 Saga 的决策和执行顺序逻辑分布在 Saga的每一个参与方中,它们通过交换事件的方式来进行沟通。例如AT模式。
步骤:
- Order服务创建一个Order并发布OrderCreated事件。
- Consumer服务消费OrderCreated事件,验证消费者是否可以下订单,并发布ConsumerVerified事件。
- Kitchen服务消费OrderCreated事件,验证订单,在CREATE_PENDING状态下创建故障单,并发布TicketCreated事件。
- Accounting服务消费OrderCreated事件并创建一个处于PENDING状态的Credit CardAuthorization。
- Accounting服务消费TicketCreated和ConsumerVerified事件,向消费者的信用卡收费,并发布信用卡授权失败事件。
- Kitchen服务使用信用卡授权失败事件并将故障单的状态更改为REJECTED。
- 订单服务消费信用卡授权失败事件,并将订单状态更改为已拒绝。
2、编排式
把Saga的决策和执行顺序逻辑集中在一个Saga编排器类中。Saga 编排器发出命令式消息给各个 Saga 参与方,指示这些参与方服务完成具体操作(本地事务)。类似于一个状态机,当参与方服务完成操作以后会给编排器发送一个状态指令,以决定下一步做什么。
步骤:
- Order Service首先创建一个Order和一个创建订单控制器。
- Saga orchestrator向Consumer Service发送Verify Consumer命令。
- Consumer Service回复Consumer Verified消息。
- Saga orchestrator向Kitchen Service发送Create Ticket命令。
- Kitchen Service回复Ticket Created消息。
- Saga协调器向Accounting Service发送授权卡消息。
- Accounting服务部门使用卡片授权消息回复。
- Saga orchestrator向Kitchen Service发送Approve Ticket命令。
- Saga orchestrator向订单服务发送批准订单命令。
2)补偿策略
Saga最重要的是如何处理异常,状态机还定义了许多异常状态。一个Saga由三种不同类型的事务组成:可补偿性事务(可以回滚,因此有一个补偿事务);关键性事务(这是 Saga的成败关键点,比如4账户代扣);以及可重复性事务,它不需要回滚并保证能够完成。
在Create Order Saga 中,createOrder()、createTicket()步骤是可补偿性事务且具有撤销其更新的补偿事务。
verifyConsumerDetails()事务是只读的,因此不需要补偿事务。
authorizeCreditCard()事务是这个 Saga的关键性事务。如果消费者的信用卡可以授权,那么这个Saga保证完成。
approveTicket()和approveRestaurantOrder()步骤是在关键性事务之后的可重复性事务。
CQRS
**CQRS 的核心思想是将这两类不同的操作进行分离,然后在两个独立的「服务」中实现。这里的「服务」一般是指两个独立部署的应用。在某些特殊情况下,也可以部署在同一个应用内的不同接口上。**Command 与 Query 对应的数据源也应该是互相独立的,即更新操作在一个数据源,而查询操作在另一个数据源上。所以当 command 系统完成数据更新的操作后,会通过「领域事件」的方式通知 query 系统。query 系统在接受到事件之后更新自己的数据源。所有的查询操作都通过 query 系统暴露的接口完成。
面临的问题
1)事务问题
2)基础设施与技术能力的挑战。
3)没有一个成熟易用的框架
Axon 可能算一个,但是 Axon 本身是一个重量级且依赖性较高的框架。为了 CQRS 而引入 Axon 有点舍本逐末的意思,因此大部分时间你不得不自己动手实现 CQRS。另外据我了解Axon领域事件和聚合根是一对一关系,与现有业务场景不匹配。
4)不同类型的数据存储引擎
由于分离了领域模型与数据模型,因此意味着我们可以在 Query 端使用与查询需求更为贴近的数据存储引擎,例如 NoSQL,ElasticSearch 等。遇到性能问题,就可以选择对应的数据查询方案,且不影响业务。
1、架构示例
1、文件云端存储业务的事件以及处理器时序图
2、事件注册示例
package domain;
import domain.event.DomainEvent;
import domain.handler.event.DomainEventHandler;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: 事件注册逻辑
* @Author: zhangwenbo
* @Since: 2019/3/6
*/
public class DomainRegistry {
private Map<String, List<DomainEventHandler>> handlerMap =
new HashMap<String, List<DomainEventHandler>>();
private static DomainRegistry instance;
private DomainRegistry() {
}
public static DomainRegistry getInstance() {
if (instance == null) {
instance = new DomainRegistry();
}
return instance;
}
public Map<String, List<DomainEventHandler>> getHandlerMap() {
return handlerMap;
}
public List<DomainEventHandler> find(String name) {
if (name == null) {
return null;
}
return handlerMap.get(name);
}
//事件注册与维护,register分多少个场景根据业务拆分,
//这里是业务流的核心。如果多个事件需要维护前后依赖关系,
//可以维护一个priority逻辑
public void register(Class<? extends DomainEvent> domainEvent,
DomainEventHandler handler) {
if (domainEvent == null) {
return;
}
if (handlerMap.get(domainEvent.getName()) == null) {
handlerMap.put(domainEvent.getName(), new ArrayList<DomainEventHandler>());
}
handlerMap.get(domainEvent.getName()).add(handler);
//按照优先级进行事件处理器排序
。。。
}
}
3、文件上传完毕事件示例
package domain.handler.event;
import domain.DomainRegistry;
import domain.StateDispatcher;
import domain.entity.meta.MetaActionEnums;
import domain.event.DomainEvent;
import domain.event.MetaEvent;
import domain.repository.meta.MetaRepository;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
* @Description:一个事件操作的处理器
* 我们混合使用了Saga的两种模式,外层事件交互;
* 对于单个复杂的事件内部采取状态流转实现。
* @Author: zhangwenbo
* @Since: 2019/3/6
*/
@Component
public class MetaConfirmUploadedHandler implements DomainEventHandler {
@Resource
private MetaRepository metaRepository;
public void handle(DomainEvent event) {
//1.我们在当前的上下文中定义个ThreadLocal变量
//用于存放事件影响的聚合根信息(线程共享)
//2.当然如果有需要额外的信息,可以基于event所
//携带的信息构造Specification从repository获取
// 代码示例
// metaRepository.queryBySpecification(SpecificationFactory.build(event));
DomainEvent domainEvent = metaRepository.load();
//此处是我们的逻辑
。。。。
//对于单个操作比较复杂的,可以使用状态流转进一步拆分
domainEvent.setStatus(nextState);
//在事件触发之后,仍需要一个状态跟踪器来解决大事务问题
//Saga编排式
StateDispatcher.dispatch();
}
@PostConstruct
public void autoRegister() {
//此处可以更加细分,注册在哪一类场景中,这也是事件驱动的强大、灵活之处。
//避免了if...else判断。我们可以有这样的意识,一旦你的逻辑里面充斥了大量
//switch、if的时候来看看自己注册的场景是否可以继续细分
DomainRegistry.getInstance().register(MetaEvent.class, this);
}
public String getAction() {
return MetaActionEnums.CONFIRM_UPLOADED.name();
}
//适用于前后依赖的事件,通过优先级指定执行顺序
public Integer getPriority() {
return PriorityEnums.FIRST.getValue();
}
}
4、事件总线示例
package domain;
import domain.event.DomainEvent;
import domain.handler.event.DomainEventHandler;
import java.util.List;
/**
* @Description:
* @Author: zhangwenbo
* @Since: 2019/3/6
*/
public class DefaultDomainEventBus {
public static void publish(DomainEvent event, String action,
EventCallback callback) {
List<DomainEventHandler> handlers = DomainRegistry.getInstance().
find(event.getClass().getName());
handlers.stream().forEach(handler -> {
if (action != null && action.equals(handler.getAction())) {
Exception e = null;
boolean result = true;
try {
handler.handle(event);
} catch (Exception ex) {
e = ex;
result = false;
//自定义异常处理
。。。
} finally {
//write into event store
saveEvent(event);
}
//根据实际业务处理回调场景,DefaultEventCallback可以返回
if (callback != null) {
callback.callback(event, action, result, e);
}
}
});
}
}
2、CQRS命令查询的责任分离
命令(Command):不返回任何结果(void),但会改变对象的状态。
查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用
Q端轻量级查询:多种不同的查询视图通过订阅事件来更新
C端通过分布式消息队列水平扩展,天然支持削峰
3、CQRS架构的缺点
1)不是强一致性,而是面向最终一致性
2)强依赖高性能可靠的分布式消息队列
3)必须有强大可靠的CQRS框架,从头做起成本高、风险大
4)最好结合Event Sourcing模式,否则CQ分离意义不大
4、使用约束
1)命令一次只能修改一个或者多个聚合根,若是多个聚合根,则需要通过消息队列实现(看领域模型定义)
2)聚合间是否只能通过领域消息交互(看框架底层设计,底层如果隐式转换处理那么后续引发的事务相关问题相对少,局限性小)
3)聚合内强一致性
4)聚合间最终一致性
5、框架特色
1)实现CQRS架构,解决CQRS架构的C端的高并发写的问题,以及CQ两端数据同步的顺序性保证和幂等性,支持C端完成后立即返回Command的结果,也支持CQ两端都完成后才返回Command的结果
2)聚合根常驻内存(In-Memory Domain Model),设计上尽可能的避免了聚合根重建,可以完全以OO的方式来设计实现聚合根,不必为ORM的阻抗失衡而烦恼
3) 基于聚合根ID + 事件版本号的唯一索引,实现聚合根的乐观并发控制
4) 通过聚合根ID对命令或事件进行路由,聚合根的处理基于Actor思想,做到最小的并发冲突、最大的并行处理,Group Commit Domain event
5)架构层面严格规范了开发人员该如何写代码,和DDD开发紧密结合,严格遵守聚合内强一致性、聚合之间最终一致性的原则
6) 先进的Saga机制,以事件驱动的流程管理器(Process Manager)的方式支持一个用户操作跨多个聚合根的业务场景,如订单处理,从而避免分布式事务的使用
7) 基于 Event Sourcing 的思想持久化C端的聚合根的状态,让C端的数据持久化变得通用化
8)在设计上完全与IoC容器解耦,同时保留了扩展性,可以适配了SpringCloud
9)通过基于分布式消息队列横向扩展的方式实现系统的可伸缩性(基于队列的动态扩容/缩容),接口抽象极简,只要求最基础的队列能力,目前适配了Kafka、RocketMQ(ONS)、Pulsar
10)EventStore内置适配了JDBC、MySQL、PostgreSQL、MongoDB存储,可针对性实现对应扩展
6、EDA 事件驱动架构(专注于微服务)
**EDA 事件驱动架构( Event-Driven Architecture ) 是一种系统架构模型,它的核心能力在于能够发现系统“事件”或重要的业务时刻(例如交易节点、站点访问等),并实时或接近实时地对相应的事件采取必要行动。**这种模式取代了传统的“ request/response ”模型,在这种传统架构中,服务必须等待回复才能进入下一个任务,事件驱动架构的流程是由事件提供运行的。
1)经典 EDA 事件驱动:
事件总线(EventBridge)最重要的能力是通过连接应用程序,云服务和 Serverless 服务构建 EDA(Event-driven Architectures) 事件驱动架构,驱动应用与应用,应用与云的连接。
2)流式 ETL 场景。
EventBridge 另一个核心能力是为流式的数据管道的责任,提供基础的过滤和转换的能力,在不同的数据仓库之间、数据处理程序之间、数据分析和处理系统之间进行数据同步/跨地域备份等场景,连接不同的系统与不同服务。
3)统一事件通知服务:
EventBridge 提供丰富的云产品事件源与事件的全生命周期管理工具,可以通过总线直接监听云产品产生的数据,并上报至监控,通知等下游服务。
7、CQRS与Event Sourcing的关系
在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回。在操作(Command)方面,是通过发送Command实现,由CommandBus处理特定的Command,然后由Command将特定的Event发布到EventBus上,然后EventBus使用特定的Handler来处理事件,执行一些诸如,修改,删除,更新等操作。
所有与Command相关的操作都通过Event实现。这样可以通过记录Event来记录系统的运行历史记录,并且能够方便的回滚到某一历史状态。Event Sourcing就是用来进行存储和管理事件的。
小结
查询(query)/变更(command)
1、查询 – 网关路由 – 监听器(query) – 处理器 – 返回resultDTO
2、数据变更 – 监听器(监听所有) – 领域模型(定义命令与领域、以及领域聚合的关系)-- 监听器(command) – command编排器 - -command转换器 – command处理器 – 保存到限界上下文 – 监听聚合根的变化 – domain --监听器(event) – event编排器 – event处理器
2-1、event处理器 – event溯源(ES)
2-2、event处理器 – db(分布式事务seata)-- Command总监听器 – 返回resultDTO
脚手架编码规范
1、Api模块编码规范:
- Api模块是专门用于定义对外接口的模块,所以这个模块中只包含接口定义,出入参定义,尽量不依赖其他包。
- Api中的接口定义类以xxxxResource(或者xxxxService)结尾。这条规范完全是为了和老的应用保持一致。
- Api接口的入参尽量不要使用Java中的原子类型(Primitive Type), 需要将入参定义为单独的类。 最好是继承现有的BaseRequest类。
- Api接口的出参统一使用泛型类对真实的返回类型进行包装。
- 出入参类都以DTO结尾。
- 出入参中尽量不适用枚举值类型的成员变量。
2、Adapter/Controller模块编码规范:
- 这一层中需要将出入参的DTO和业务层的VO/DO对象进行转换。
- 这一层不要包含任何的业务逻辑,只包含参数转换和业务无关的校验逻辑。
- 接口返回值缓存类的逻辑,可以放在这个模块中实现,因为这个动作不包含业务逻辑。
3、App模块编码规范:
- 这个模块中的类统一以Case结尾。
- 这一层主要是对底层业务逻辑进行编排。可以直接调用Domain层的port定义。跨域的服务调用也可以放在这个模块中。
- 这一层可以直接调用Domain模块中定义的Repository服务。
- 事务处理:如果是跨多个聚合的业务逻辑需要放在一个事务中,需要在这一层开启和提交事务。
4、Domain层编码规范:
- DomainService命名统一以Service为后缀。
- Entity实体类的命名不用后缀。 值对象类的定义统一以VO结尾。
- DomainService逻辑中可以调用Repository和Port中定义的接口。
- DomainService可以操作多个聚合,实体和值对象。
- Entity实体类可以有构造函数,builder,getters。 不要直接放开所有属性的setters,防止业务代码随意修改实体的属性。
- 编写业务逻辑需要遵守原则:优先将业务逻辑放在Entity和VO中,然后才是放在聚合中,最后才放在DomainService中。
- 依赖反转原则:Domain层依赖的外部接口都要定义在Domain模块的port包中。Domain层只面向接口编程,不依赖接口实现类。
5、Adapter/Repository和Rpc模块编码规范:
- Repository实现类中需要将接口入参中的DO对象转换为PO对象后再调用数据库存储。
- Repository和聚合的关系是一对一的关系。一个Repository有唯一的对应的聚合。
- 如果Repository中需要开始事务可以在Repository实现类中开启事务。
- Rpc层最好是对外部接口的出参和入参定义一个防腐层对象,命名统一以DTO结尾。
总结
**DDD最大的价值就是在梳理业务的时候划分清楚业务领域的边界,其核心思想其实还是高内聚低耦合而已。**至于工程方面,现在微服务的粒度已经足够细,条件允许的情况下,复杂的业务可以更加细粒度一点。另外一个优点就是,DDD架构已经固化了开发者编码思维,有利于产品的迭代,减少了屎山一样的代码。缺点也很明显,团队必须要与领域专家建立一个良好的沟通,统一思想、规范。