二. DDD和微服务编码实战
4. 事件风暴
4.1 四色原型/彩色建模
定义4种类的原型,在UML中用不同颜色表示不同原型的对象:
- Party, Place, Thing Party: 事件的参与方,例如某人人、某组织等 Place: 事件的发生地,例如仓库、零售店铺 Thing: Thing classes are those that identify individual items such as individual cars, airplanes, DVD's, books, pieces of equipment, etc. 按照字面意思理解,应当是指事件中具体的物品,比如客户的购买事件中,thing可能不是指订单(order),而是订单中具体的物品,例如图书、衣服等
- Role,角色,对party, place, thing的参与行为、方式的抽象
- Moment-Interval models something that one needs to work with and track for business and legal reasons, something that occurs at a moment in time or over an interval of time. 指业务的活动、对象等,因业务需要和法律原因等需要操作、记录、追踪的东西,例如销售、订单、预定、航班、会议、行程等
- Description 对某一类型事物的额外描述数据,例如产品有各种基本属性,但各种类型的产品差异比较大,比如做电子商务系统时,图书、服装、眼镜等不同类型的商品,在前台购买流程、后端订单处理流程、财务记账处理、库存管理方式、售后服务原则等各方面都会存在比较大的差异,这些差异按照产品类型设计成产品的额外描述属性,这些被称为description 其效果就是,当你看到产品主对象时,可以明确的知道他代表什么,他的其他附属资料则在description中。大部分地方可能使用的是产品基本属性,只需关心产品主对象;涉及到差异处理的部分才需要关心description相关数据。因此将其分成不同的原型类型,使用不同颜色表示
不同的原型
- 在UML图中采用不同的颜色表示,便于对复杂的UML图的理解
- 具有各自的职责
- 可能具备一些共性,例如相同的属性、操作,以及相互之间的关系等,至少在设计时这些方面应当考虑
4.2 通过事件风暴协作建模
4.3 事件风暴示例 - 事件
我作为用户,签到任务会奖励10个积分,我完成签到后,系统生成了一个积分充值申请;过了一会儿,积分账户余额增加了,充值申请也变成了完成状态。
- |充值申请已受理| -> |账户余额已增加| -> |充值申请已完成|
我作为用户,购买一个商品,可以用100积分抵扣1元钱;当我提单的时候,首先账户余额部分(100)被锁定,积分授信成功;订单提交成功后,请求核销授信,然后,账户锁定余额转为扣减,最后,授信结束。
- |授信申请中| -> |账户部分余额已被锁定| -> |授信已成功|
- |授信核销中| -> |授信核销中| -> |授信已核销|
4.4 事件风暴示例 - 命令
- |会员系统| -> |充值| -> |充值申请已受理| -> |账户余额已增加| -> |充值申请已完成|
- |交易系统| -> |申请授信| -> |授信申请中| -> |账户部分余额已被锁定| -> |授信已成功|
- |交易系统| -> |核销授信| -> |授信核销中| -> |账户锁定已转扣减| -> |授信已核销|
4.5 事件风暴示例 - 聚合根
4.6 事件风暴示例 - 代码
4.7 事件风暴示例 - 代码
-
发起充值请求
-
增加账户余额
-
完成充值请求
5. 聚合根和数据一致性
应用服务作为事务一致性边界,一个事务里不能涉及到两个聚合的修改,跨聚合的数据应该使用最终一致。
但最终一致性成本很高。
前面的例子里,基于内存实现了同步的领域事件发布和订阅。这样,实际上两个聚合根的更改基于同一个本地数据库事务。
但由于使用了事件驱动,在代码层面,两个聚合根的更新是解耦的,在需要最终一致性的时候容易重构。
绿色的是需新增的,红色的是需修改的。没有改的业务层(应用服务和领域模型)的任何代码;通过事件收件箱实现了事务性消息的可靠发送,账户事件表通过“充值请求ID+事件类型”做唯一约束实现了防重。
6. 应用层和领域层的职责
判断一系列交互是否属于领域层的一种方法是问:“这是否总是会发生?” 或“这些步骤是分不开的吗?”
-
如果是这样,那听起来像是领域逻辑里的策略,因为这些步骤始终必须同时进行。
-
如果可以以多种方式组合这些步骤,则可能不是领域概念。
7. 领域服务
-
当领域逻辑放某一个聚合里不合适,需要协调多个聚合,但由于是领域逻辑,放在应用服务里不合适的时候,可以放到领域服务里.
-
需要访问数据库等外部资源的业务逻辑,不建议聚合里,可以放到领域服务里.
-
有些算法、策略代码,为了保持实体和值对象的职责单一,可以提炼出来变成领域服务(领域服务类的命名不一定都要以Service结尾).
8. 资源库的实现方式
8.1 聚合和资源库聚合原则
一个聚合有一个资源库,其它实体没有.
通过“Unit of Work”的方式简化聚合作为整体的持久化工作.
8.2 资源库的实现方式 - JPA/Hibernate
在内存中维护并跟踪对象的状态:
Hibernate带来的复杂性:
Lazyloading和N+1问题/session关闭问题
Many-to-Many映射
不同的FetchModes
• 不同的FetchModes
我们需要更简单的ORM,可以通过ArchUnit裁剪Hibernate的特性。比如禁止聚合根间的关联、禁止使用Many-to-Many映射、禁止使用Lazyloading等.
8.3 资源库的实现方式 - Spring-Data-JDBC
官网地址
- 简单和可定制性强
- 原生支持DDD
8.4 资源库的实现方式 - JDBC/Mybatis
需要自己管理聚合作为一个整体的持久化工作,比如使用备忘录模式,记录快照等.
9. 业务逻辑之外
- CQRS
- 组合UI
- 报表
9.1 CQRS - 命令查询职责分离
CQRS:
Command Query Responsibility Segregation (命令查询职责分离).
问题:
上一章的那个例子,一个Account是一个聚合根,一个充值记录作为子对象,如果我有一个业务场景是查询所有的充值记录,不是一个Account的充值记录,是所有的Account的充值记录,那:
这个需求还是用Account作为聚合根吗?
如果还是用Account作为聚合根,查询充值记录不是要很多的内存?
还是要新设计一个新的领域模型?这样设计的话,领域模型会不会太多?
是不是一个领域模型设计是不是只能针对一个场景设计
-
在建立命令模型之外建立查询模型
-
通过异构数据增加查询的灵活性和性能
9.2 UI和报表
Domain Model和Repository不应该为复杂的查询和报表而设计。
报表应基于数据模型而非领领域对象模型。
一一对应带来体验僵化:
| UI1 | | UI2 | | UI3 |
|服务1| |服务2| |服务3|
体验应该跨越服务边界:
| UI1 | | UI2 |
|服务1| |服务2| |服务3|
9.3 事件溯源
Event Source: 事件溯源
如第六步,将事件存储起来, 可以进行事件回放.