领域驱动设计(三)

1,014 阅读7分钟

二. DDD和微服务编码实战

4. 事件风暴

4.1 四色原型/彩色建模

四色原型/彩色建模

​ 定义4种类的原型,在UML中用不同颜色表示不同原型的对象:

  1. 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),而是订单中具体的物品,例如图书、衣服等
  2. Role,角色,对party, place, thing的参与行为、方式的抽象
  3. 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. 指业务的活动、对象等,因业务需要和法律原因等需要操作、记录、追踪的东西,例如销售、订单、预定、航班、会议、行程等
  4. Description 对某一类型事物的额外描述数据,例如产品有各种基本属性,但各种类型的产品差异比较大,比如做电子商务系统时,图书、服装、眼镜等不同类型的商品,在前台购买流程、后端订单处理流程、财务记账处理、库存管理方式、售后服务原则等各方面都会存在比较大的差异,这些差异按照产品类型设计成产品的额外描述属性,这些被称为description 其效果就是,当你看到产品主对象时,可以明确的知道他代表什么,他的其他附属资料则在description中。大部分地方可能使用的是产品基本属性,只需关心产品主对象;涉及到差异处理的部分才需要关心description相关数据。因此将其分成不同的原型类型,使用不同颜色表示

不同的原型

  1. 在UML图中采用不同的颜色表示,便于对复杂的UML图的理解
  2. 具有各自的职责
  3. 可能具备一些共性,例如相同的属性、操作,以及相互之间的关系等,至少在设计时这些方面应当考虑

2_8.四色原型.png

4.2 通过事件风暴协作建模

2_9.通过事件风暴协作建模.png

4.3 事件风暴示例 - 事件

​ 我作为用户,签到任务会奖励10个积分,我完成签到后,系统生成了一个积分充值申请;过了一会儿,积分账户余额增加了,充值申请也变成了完成状态。

  • |充值申请已受理| -> |账户余额已增加| -> |充值申请已完成|

​ 我作为用户,购买一个商品,可以用100积分抵扣1元钱;当我提单的时候,首先账户余额部分(100)被锁定,积分授信成功;订单提交成功后,请求核销授信,然后,账户锁定余额转为扣减,最后,授信结束。

  • |授信申请中| -> |账户部分余额已被锁定| -> |授信已成功|
  • |授信核销中| -> |授信核销中| -> |授信已核销|

4.4 事件风暴示例 - 命令

  • |会员系统| -> |充值| -> |充值申请已受理| -> |账户余额已增加| -> |充值申请已完成|
  • |交易系统| -> |申请授信| -> |授信申请中| -> |账户部分余额已被锁定| -> |授信已成功|
  • |交易系统| -> |核销授信| -> |授信核销中| -> |账户锁定已转扣减| -> |授信已核销|

4.5 事件风暴示例 - 聚合根

2_10.事件风暴示例 - 聚合根.png

4.6 事件风暴示例 - 代码

2_11.事件风暴示例 - 代码.png

4.7 事件风暴示例 - 代码

  1. 发起充值请求

2_12.发起充值请求.png

  1. 增加账户余额

2_13.增加账户余额.png

  1. 完成充值请求

2_14.完成充值请求.png

5. 聚合根和数据一致性

​ 应用服务作为事务一致性边界,一个事务里不能涉及到两个聚合的修改,跨聚合的数据应该使用最终一致。

​ 但最终一致性成本很高。

​ 前面的例子里,基于内存实现了同步的领域事件发布和订阅。这样,实际上两个聚合根的更改基于同一个本地数据库事务。

​ 但由于使用了事件驱动,在代码层面,两个聚合根的更新是解耦的,在需要最终一致性的时候容易重构。

2_15.聚合根和数据一致性.png

​ 绿色的是需新增的,红色的是需修改的。没有改的业务层(应用服务和领域模型)的任何代码;通过事件收件箱实现了事务性消息的可靠发送,账户事件表通过“充值请求ID+事件类型”做唯一约束实现了防重。

6. 应用层和领域层的职责

2_16.应用层和领域层的职责.png

2_17.应用层和领域层的职责.png

​ 判断一系列交互是否属于领域层的一种方法是问:“这是否总是会发生?” 或“这些步骤是分不开的吗?”

  • 如果是这样,那听起来像是领域逻辑里的策略,因为这些步骤始终必须同时进行。

  • 如果可以以多种方式组合这些步骤,则可能不是领域概念。

7. 领域服务

  • 当领域逻辑放某一个聚合里不合适,需要协调多个聚合,但由于是领域逻辑,放在应用服务里不合适的时候,可以放到领域服务里.

  • 需要访问数据库等外部资源的业务逻辑,不建议聚合里,可以放到领域服务里.

  • 有些算法、策略代码,为了保持实体和值对象的职责单一,可以提炼出来变成领域服务(领域服务类的命名不一定都要以Service结尾).

8. 资源库的实现方式

8.1 聚合和资源库聚合原则

​ 一个聚合有一个资源库,其它实体没有.

2_20.聚合&资源库.png

​ 通过“Unit of Work”的方式简化聚合作为整体的持久化工作.

2_21.聚合&资源库2.png

8.2 资源库的实现方式 - JPA/Hibernate

​ 在内存中维护并跟踪对象的状态:

2_22.hibernate.png

​ 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

​ 需要自己管理聚合作为一个整体的持久化工作,比如使用备忘录模式,记录快照等.

2_23.mybatis.png

9. 业务逻辑之外

  • CQRS
  • 组合UI
  • 报表

9.1 CQRS - 命令查询职责分离

CQRS:

​ Command Query Responsibility Segregation (命令查询职责分离).

martinfowler.com/bliki/CQRS.…

问题:

​ 上一章的那个例子,一个Account是一个聚合根,一个充值记录作为子对象,如果我有一个业务场景是查询所有的充值记录,不是一个Account的充值记录,是所有的Account的充值记录,那:

  • 这个需求还是用Account作为聚合根吗?

  • 如果还是用Account作为聚合根,查询充值记录不是要很多的内存?

  • 还是要新设计一个新的领域模型?这样设计的话,领域模型会不会太多?

  • 是不是一个领域模型设计是不是只能针对一个场景设计

  1. 在建立命令模型之外建立查询模型

2_24.CQRS.png

  1. 通过异构数据增加查询的灵活性和性能

2_25.CQRS.png

9.2 UI和报表

​ Domain Model和Repository不应该为复杂的查询和报表而设计。

​ 报表应基于数据模型而非领领域对象模型。

一一对应带来体验僵化:

​ |  UI1  | |  UI2  | |  UI3  |

​ |服务1| |服务2| |服务3|

体验应该跨越服务边界:

​ |       UI1       |  |       UI2       |

​ |服务1| |服务2| |服务3|

9.3 事件溯源

2_26.事件溯源.png

Event Source: 事件溯源

如第六步,将事件存储起来, 可以进行事件回放.