# 总览分析
项目结构
- interfaces:用户表示层,最顶层;负责向用户显示信息和解释用户命令,请求应用层以获取用户所需要展现的数据,发送命令给应用层要求其执行某个用户命令;
- facade:门面,为远程客户端提供粗粒度的调用接口,将一个用户请求委派给一个或多个Service进行处理;
- DTO:数据传输对象,是与外部通讯的载体,是一个纯粹的POJO,内部不应该包含任何的业务逻辑;
- assembler:装配器,实现DTO与领域对象之间的相互转换,与DTO同时出现;
- 各种协议入口:如servlet、controller、mq协议,这种分层直接调用facade门面的入口;
- application:应用层,回答微服务应用要完成的任务内容,要求尽量的简单,不包含任何的业务逻辑或者知识,事务均放在应用层做处理;
- 各种service:为一个接口,就是应用层接口
- serviceImpl:为应用层接口的具体实现
- event:为对应的事件接口
- domain:领域层,主要负责表达业务概念,业务状态信息和业务规则,几乎所有的业务逻辑均在该层实现
- entity:实体,具有为已标志的对象
- value object:值对象,无需唯一标志的对象
- service(domain service):领域服务,一些行为无法归类到实体对象或值对象上,本质是一些操作而非事物
- aggregate:聚合根,一组具有内聚关系的相关对象或者集合
- factory:工厂,创建复杂对象时,隐藏创建的细节
- repository:仓储,提供查找和持久化对象的方法
- infrastructure:基础设施层,与所有层相交互,为用户表示层提供组件配置,为应用层提供传递消息的能力,为领域层提供持久化机制
- eventImpl:应用层的event实现
- persistence:持久化实现,为对应领域层中repository的实现
# DDD共识
DDD是天生指读写分离的,原则上它只限制了写操作的准则,而不对读操作有太多的指导和干预;所以我们在讨论DDD的时候一般讲的都是关注如何进行写操作落地与建模的,而读操作则是可以进行传统的MVC结构,从存储结构层面数据直出
要求在代码编写时:进行外部逻辑的入侵屏蔽,对内部逻辑暴露的细节抹除;假设外部引用是污浊的,内部域内代码是干净的,我们仅能通过一些防腐层做好隔离处理,所以我们可以看到很多参考文章中写xxxFacade,xxxGateway,都是为了防止外部逻辑对内部域模型的侵入,以及内部域对外部世界的暴露
DDD是需要堆叠很多代码量的,不能由于一时的简单快捷而破环DDD的指导准则
架构扁平,分离接口与具体实现,面向接口编程
分离技术与业务
DDD有很多落地的思想与架构,例如有:
- 洋葱模型
- 整洁架构
- 六边形架构
- 菱形对称模型
- COLA模型
# DDD讲解
adapter
- 表示对外部调用接口调用的适配,而不是实现
- 这里的接口尽量小,尽量细,不能大而全
- 这里只有对应的interface,并没有对应的实现
- 出入参数可以为内部的domain的模型,也可以为基本类型
例如工单领域需要查询知识点领域的内容:根据知识点id批量查询知识点目录
public interface KnowledgeAdapter { /** * 获取知识点目录名称 * * @param ids 知识点目录id * @return 名称集合 */ Map<Integer, String> getKnowledgeCategoryNames(Set<Integer> ids); }
1
2
3
4
5
6
7
8
9application
- 应用层,回答服务应用要完成的任务内容,要求尽量的简单
- 不包含任何的业务逻辑或者知识
- 没有if...else等逻辑判断(业务逻辑)
- 没有计算相关的东西,例如计算工单时长等,都不能出现
- 应用层分为两类:
- xxxQueryService:表示读取,入参都是以xxxQuery结尾,出餐xxxDTO结尾
- xxxApplicationService:表示写入,入参都是以xxxCommand结尾,如果创建了聚合根对象则返回对应的id,否则返回值为空
- 这里是做数据库事务的地方
- 原则上这里的接口是意识不到事务的,即不能再接口层面上面加@Transational的注解
- 注意长事务,如果有长事务出现则需要手动编排管理事务
- 这里只有对应的interface,并没有对应的实现
- 编写application实现原则:
- 数据校验
- 通过Repository查询聚合根
- 操作聚合根,对聚合根进行状态的变更
- 通过Repository保存聚合根
- 发送领域事件
public interface OrderQueryService { /** * 查询工单详情 * * @param query 查询内容 * @return 工单详情 */ GetOrderDTO getOrder(@Valid @NotNull GetOrderQuery query); /** * 查询我的锁定中的工单 * * @param query 查询内容 * @return 工单列表 */ PageResult<ListMyOrderDTO> listMyOrder(@Valid @NotNull ListMyOrderQuery query); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17domain
- DDD所重点关注的地方
- 是整个领域的核心,也是MVP(Minimum Viable Product最简化可实行产品)
- 不关心和提现技术细节,只体现业务价值
- 能够独立进行测试运行
- 除了内部依赖内部工具库以外,不依赖其他库或者框架
- 里面按照领域进行分包,一个领域独占一个包
biz
- 是领域的业务逻辑
- 不是DDD所规范必须的
- 可以用设计模式手段来实现业务价值
entity
- 类是有血有肉富含行为的,不再是单纯的POJO
- 有对应的id,一个Entity对应有一个唯一的id
- 判断两个Entity是否相等应该直接判断id
- id需要用一个对象进行包裹,防止id的唯一性变更
- 一个聚合根对应有一个Repository
- 封装业务的参数校验以及业务逻辑
- 写方法一般来说返回是void,可以直接扔出领域事件,让application层进行事件抛出
@Getter @ToString @EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Order implements AggregateRoot<OrderId>, Serializable { /** * 工单主键 */ private OrderId id; // 这里是一些比较重要的设计点 /** * 所属组别 */ private GroupId groupId; /** * 优先级,这个值和提单人是否是VIP,以及工单的紧急度相关 */ private Priority priority = Priority.LOWEST; /** * 锁定信息 */ private Lock lock = NO_LOCK; /** * 工单归档信息 */ private ArchiveInformation archiveInformation = ArchiveInformation.NOT_ARCHIVED; /** * 状态 */ private OrderStatusEnum status = WAITING; /** * 业务id */ private Integer appId; /** * 服务id */ private Long serviceId; /** * 工单来源,1-聊天入口,2-电话入口,3-手动提单 */ private OriginEnum origin; /** * 接受/锁定工单 * * @param user 接受工单的客服人员 * @param permanent 是否永久锁定 */ public List<Event> lock(UserId user, boolean permanent) { checkNotLocked(); this.setLock(this.lock.lock(user, permanent)); EventResult<OrderStatusEnum> eventResult = OrderTypeProcessorFactory.getProcessor(this.getType()).lock(this); List<Event> events = new ArrayList<>(); events.addAll(eventResult.getEvents()); events.addAll(changeStatusTo(eventResult.getResult())); maintainedBy(user); return events; } /** * 解锁/释放工单 * * @param user 释放工单的客服人员 */ public List<Event> unlock(UserId user) { checkOrderStatusIn(WAITING, FOLLOWING); checkLockedBy(user); this.setLock(NO_LOCK); EventResult<OrderStatusEnum> eventResult = OrderTypeProcessorFactory.getProcessor(this.getType()).unlock(this); List<Event> events = new ArrayList<>(); events.addAll(eventResult.getEvents()); events.addAll(changeStatusTo(eventResult.getResult())); maintainedBy(user); return events; } /** * 解决/归档工单 * * @param user 释放工单的客服人员 * @param archiveType 结单类型 */ public List<Event> archive(UserId user, ArchiveTypeEnum archiveType) { checkOrderStatusIn(WAITING, FOLLOWING); checkLockedBy(user); this.setArchiveInformation(ArchiveInformation.of(user, archiveType)); this.setLock(this.lock.unlock()); EventResult<OrderStatusEnum> eventResult = OrderTypeProcessorFactory.getProcessor(this.getType()).archive(this); List<Event> events = new ArrayList<>(); events.addAll(eventResult.getEvents()); events.addAll(changeStatusTo(eventResult.getResult())); // 如果设定给用户确认,那么工单有可能会成为二次工单,此时要设定初始化对应的二次状态 if (this.getReplyConfig().isCheckResult()) { ReopenInformation secondaryOrder = this.getReopenInformation().waitToClientConfirm(); this.setReopenInformation(secondaryOrder); } maintainedBy(user); return events; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111repository
- 保存聚合根的状态
- 本质上只有save和find两种的方法
- 这里只有对应的interface,并没有对应的实现
service
- 领域服务,请与ApplicationService区分开
- domain service是一个不必要的妥协,应该越少越好
- 有些行为不能单纯地放在一个实体中
- 对外围接口进行调用的情况
- 未来复杂的计算逻辑,但是计算的参数有预想地不再聚合内
- 一些同时改变多个聚合的方法
- 不仅有接口还有实现,一些文章说直接写对应实现即可,但是本人还是推荐保留接口
/**
* 计算工单优先级
* 由于工单优先级可能有多个影响的因素,如:创建的时间、紧急度、工单创建人的职位、是否VIP等
* 这些不定的因素都会
*/
public interface OrderPriorityCalculateService {
/**
* 计算优先级
*
* @param order 工单
* @return 优先级
*/
Priority calculate(Order order);
}
/**
* 工单发送通知给用户
*/
public interface ReplyService {
/**
* 异步回复
*
* @param order 工单信息
* @param ways 回复方式
*/
void replyAsync(Order order, ReplyWayEnum... ways);
}
public class OrderPriorityCalculateServiceImpl implements OrderPriorityCalculateService {
/**
* 紧急度计算比重
*/
private int urgencyWeight = 2;
/**
* vip计算比重
*/
private int vipWeight = 1;
public OrderPriorityCalculateServiceImpl() {
}
@Override
public Priority calculate(Order order) {
Priority result = Priority.LOWEST;
Urgency urgency = order.getUrgency();
result = result.addPriority(Priority.of(urgency.getTimes() * urgencyWeight));
if (order.isVip()) {
result = result.addPriority(Priority.of(vipWeight));
}
return result;
}
}
public class ReplyServiceImpl implements ReplyService {
private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(4);
private final ReplyInnerAdapter replyInnerAdapter;
private final ReplyAppAdapter replyAppAdapter;
private final ReplyPushAdapter replyPushAdapter;
public ReplyServiceImpl(ReplyInnerAdapter replyInnerAdapter,
ReplyAppAdapter replyAppAdapter,
ReplyPushAdapter replyPushAdapter) {
this.replyInnerAdapter = replyInnerAdapter;
this.replyAppAdapter = replyAppAdapter;
this.replyPushAdapter = replyPushAdapter;
}
@Override
public void replyAsync(Order order, ReplyWayEnum... ways) {
ReplyConfig replyConfig = order.getReplyConfig();
ContactAccount contactAccount = order.getContactAccount();
if (!replyConfig.isReplyable() || ContactAccount.NO_CONTACT_ACCOUNT.equals(contactAccount) || ways == null || ways.length == 0) {
log.info("can not reply, maybe reply is not configured or contact account is empty!");
return;
}
OrderId id = order.getId();
UserId clientId = contactAccount.getClientId();
String replyMessage = replyConfig.getReplyMessage();
Integer appId = contactAccount.getAppId();
for (ReplyWayEnum way : ways) {
switch (way) {
case PUSH:
EXECUTOR_SERVICE.execute(() -> {
try {
PushReplyInfo pushReplyInfo = new PushReplyInfo(id, clientId, replyMessage, appId);
replyPushAdapter.sendPushMessage(pushReplyInfo);
} catch (Exception e) {
log.error("reply push message error, order: {}", order, e);
}
});
break;
case APP:
EXECUTOR_SERVICE.execute(() -> {
try {
AppReplyInfo appReplyInfo = new AppReplyInfo(id, clientId, replyMessage, appId);
replyAppAdapter.sendAppMessage(appReplyInfo);
} catch (Exception e) {
log.error("reply app message error, order: {}", order, e);
}
});
break;
case INNER:
EXECUTOR_SERVICE.execute(() -> {
try {
// 发送内部的服务信息
InnerReplyInfo innerReplyInfo = new InnerReplyInfo(id, clientId, replyMessage);
replyInnerAdapter.sendInnerMessage(innerReplyInfo);
} catch (Exception e) {
log.error("reply inner message error, order: {}", order, e);
}
});
break;
default:
throw new IllegalArgumentException();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# 目录结构(举例说明)
customer-order
│ pom.xml 项目管理文件
│ readme.md 本文文档
├─doc 文档核心文件
├─src Java标准层次目录结构
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─business
│ │ │ └─customer
│ │ │ └─order
│ │ │ ├─adapter 各种外围的adapter,例如AppAdapter
│ │ │ ├─application 应用层入口,进行读写分离
│ │ │ │ └─dto 存放与外界交互的参数以及结果(这里存放的都是对内不进行复用的)
│ │ │ │ ├─command 写命令
│ │ │ │ ├─query 读请求
│ │ │ │ └─result 返回结果
│ │ │ ├─config 一些业务的配置项,存放业务相关的spring配置文件
│ │ │ ├─constant 一些核心的配置参数(不外露的),例如:工单自动解锁的时长等
│ │ │ ├─domain 整个项目最核心的部分,存放内部业务模型,只有接口,没有实现
│ │ │ │ ├─clientsupplementation 用户补充内容领域
│ │ │ │ │ ├─entity 用户补充内容实体
│ │ │ │ │ ├─repository 用户补充内容仓储
│ │ │ │ │ └─valueobject 用户补充内容值对象
│ │ │ │ ├─order 工单领域
│ │ │ │ │ ├─biz 工单领域的业务逻辑
│ │ │ │ │ │ ├─processor 工单领域工单动作驱动器
│ │ │ │ │ │ ├─saver 工单领域工单详情保存器
│ │ │ │ │ │ └─status 工单领域工单状态驱动器
│ │ │ │ │ ├─entity 工单领域实体
│ │ │ │ │ ├─repository 工单领域仓储
│ │ │ │ │ ├─service 工单领域领域服务
│ │ │ │ │ │ └─impl 工单领域领域服务实现
│ │ │ │ │ └─valueobject 工单领域值对象
│ │ │ │ └─shared 一些共享的东西,例如像PhoneNumber、IdentityCardNumber的值对象
│ │ │ ├─infrastructure 基础设施层,是各个层的实现部分
│ │ │ │ ├─adapter adapter的实现
│ │ │ │ ├─application application的实现
│ │ │ │ └─persistence 序列化存储相关
│ │ │ │ ├─datamapper 聚合根到po或者po到聚合根的映射
│ │ │ │ ├─mybatis mybatis框架的实现
│ │ │ │ │ ├─impl mybatis-plus的ServiceImpl
│ │ │ │ │ └─mapper mybatis的mapper
│ │ │ │ ├─po 序列化对象
│ │ │ │ └─repository 仓储层的实现
│ │ │ └─interfaces 外围的数据入口
│ │ │ ├─assembler 各种转换器
│ │ │ ├─event 领域事件的处理入口
│ │ │ ├─facade 程序内部调用以及RPC调用的入口
│ │ │ ├─http 对外部网关调用的http入口
│ │ │ │ └─vo http的请求参数
│ │ │ └─schedule 定时任务的入口
│ │ └─resources 资源配置文件
│ │ └─mapper mybatis的配置文件
│ └─test 测试包
└─target 编译包
customer-order-api
│ pom.xml 项目管理文件
├─src Java标准层次目录结构
│ └─main
│ └─java
│ └─com
│ └─b
│ └─customer
│ └─order
│ └─api ` api结构
│ ├─constant 一些对外的校验常量
│ ├─dto 存放与外界交互的参数以及结果(对内进行复用的)
│ │ ├─command 写命令
│ │ ├─event 领域事件
│ │ ├─query 读请求
│ │ └─result 返回结果
│ ├─exception 异常
│ └─facade 对外暴露的接口
└─target 编译包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 资料参考
- 结合《领域驱动设计》的工程:领域驱动工程样例 (opens new window)
- 阿里DDD技术讲解:
- vivo技术讲解:
- 美团DDD技术讲解 (opens new window)
- 后端开发实践系列——领域驱动设计(DDD)编码实践 (opens new window)
- COLA技术架构 (opens new window)
- 为什么域服务必须使用域对象作为参数和返回值? (opens new window)
- 如何发布和处理领域事件 (opens new window)
- DDD 限界上下文和 Java 模块 (opens new window)
- 工厂的入参是原始对象还是value object (opens new window)
- DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together (opens new window)
# 要点
- application层只是做服务的编排,不做任何的计算逻辑
- domain service只是对象状态的变更,不做save的操作,不能注入repository
- domain service入参和出参都返回领域内的对象
- CQE对象入参全为细颗粒度
# 概述
# Interface层:
- 承接消息的入口,转化入口参数
- interface层的表达不止为http协议,也有dubbo、soap、websocket、kafka等
- 每种协议独立一套的表达方式,避免同一表达;需要注意出参要有同一的格式,例如http协议同一返回StandardReposese对象
- 应该捕捉所有异常,避免异常信息的泄漏
- 不应意识到domain层的内部对象
- 用Bean Validation做对CQE对象的校验
# Application层:
- application层做的是服务的编排,不做任何的计算逻辑;一般包含下面的操作
- 数据校验
- 通过Repository查询聚合根
- 操作聚合根,对聚合根进行状态的变更
- 通过Repository保存聚合根
- 发送领域事件
- Command、Query、Event统称为CQE,他们三者作为application的入参,根据单ID查询的场景下可以直入;统一返回DTO对象,不能暴露domain的Entity和Value Object,使用DTO Assemble进行转换
- 不同方法使用不同的CQE,因为不同方法的语义是不一样的,如果复用同一CQE对象,其中一个方法入参的变动会导致全体的参数变动
- application层需要做简单的参数校验,例如:判空、字符串合法化判断,可以用Bean Validation解决
- 有异常信息可以直接抛出,因为在上层的interface层已经捕获所有异常
- 接收domain或者domain service里面抛出的领域事件,发布对应的领域事件
# Domain层:
- Entity:
- 有对应的id,一个Entity对应有一个唯一的id
- 判断两个Entity是否相等应该直接判断id
- id需要用一个对象进行包裹,防止id的唯一性变更
- 一个聚合根对应有一个Repository
- 封装业务的参数校验以及业务逻辑
- 写方法一般来说返回是void,可以直接扔出领域事件,让application层进行事件抛出
- Value Object:
- 没有id,参数都是不可变的,若改变里面的信息需要直接new一个实体
- 没有对应的Repository
- 有对应的业务操作函数,非纯POJO
- Domain Service:
- 操作复杂的业务逻辑,往往含有两个以上的Entity的操作,如果只有操作一个Entity,可以把这些业务逻辑挪到这唯一的Entity里面
- Domain Service不应该依赖Repository,只做对Entity的状态的变更
- 注意和Application的区别,domain service是一个不必要的妥协,应该越少越好
- Repository:
- 保存Entity的状态
- 本质上只有save和find两种的方法
- 实现类完成数据库存储的细节
- Factory:
- 创建Entity对象,从0到1的过程
- 入参是领域对象,非基本类型
- 复杂构造的时候可能会依赖Repository
# Infrastructure层:
- 用ACL防腐层将外部依赖转化为内部代码,隔离外部的影响
# 使用ACL的好处
- 适配器:便于适配其他服务接口
- 缓存:可以缓存频繁请求的数据
- 兜底:防止其他服务不可用导致核心功能的不可用
- 易于测试:可以方便地通过mock和stub进行单元测试
- 功能开关:控制功能的实现
# CQE的概念与使用
Command | Query | Event | |
---|---|---|---|
语义 | "希望"能触发的操作 | 各种查询条件 | 已经发生过的事情 |
读/写 | 写 | 只读 | 写 |
返回值 | DTO或Boolean | DTO或Collection | Void |
- CQE在interfaces层做校验,推荐使用Bean Validation实现
- 不要复用CQE对象,因为不同行为后续的差异会越来越大