Skip to content

领域驱动设计:软件核心复杂性应对之道

作者:[美] 埃里克 埃文斯(Eric Evans)


开发者很容易专注于技术领域,而忽略了业务领域的核心。比如在业务设计(战略设计)过程中,不断代入各种技术,某某模块的数据库如何设计,某模块要使用 xxx 技术,并且带到业务讨论中,这不仅会给自己对业务理解带来局限,并且也会干扰业务专家的思路。

开发人员正确的做法是,先抛弃技术的思路,先深入理解业务需求,在深入且明确业务后,再来设计技术领域(战术设计)的内容。


明确一个团队只在一个限界上下文中工作,团队必须自己控制源码、数据库并定义官方接口,所有团队接入必须通过这些接口才能调用限界上下文

所以DDD是不是中台概念的出生地?


DDD 更倾向于在协作沟通中深入对业务模型进行理解,而不是通过文档


区分核心域、子域,看起来像是软件工程中的命名空间?


异步消息机制中,发布者有 2 种提供数据的思路,这两种思路需要权衡当前场景的适用情况:

增强事件:比如需要订阅者自治,一次性将全部可能的数据都推送出来

反向查询:比如有安全需求,不能一次性将全部数据提供出去

当然,这两种也可以酌情混合使用


限界上下文间的映射关系类型:

合作关系

共享内核

客户-供应商

追随者:强调服务使用者的被动适应性

防腐层(Anticorruption Layer)

开放式主机服务(Open Host Service):强调服务提供者的主动性

已发布语言(Published Language)

各行其道


我理解的DDD 中的“限界上下文”和“映射关系”:

限界上下文理解为代码中的命名空间,一个命名空间可以看作是一个业务模型领域的抽象

映射关系表示胃不同限界上下文的依赖关系,粗暴点说,就是看谁占据协作中的主动权、话语权


异步消息机制可以很好地解决集体火车事故,在实时性要求不高的情况下,使用异步消息机制是很好的选择。

集体火车事故:A 使用同步请求 B,B 使用同步请求 C,一旦 B 或则 C 崩了,那 A、B、C 都崩了


值对象,一种类似于值类型的对象。

它关注的是对象属性的值是否一致,而不在乎内存地址引用是否相同

举例:带有货币单位和金额的对象,比如一万人民币,它就不在乎保存这两个属性的对象是否地址相同。


在聚合边界内保护业务规则不变性:通过使用聚合,来实现和维护业务规则的要求

聚合要设计得小巧:聚合作为业务的最小模型,小巧既可以让实现变得简单,同时内存占用更小、效率更高,从而提高事务成功率;设计时要注意使用 SRP(单一职责原则)

只能通过标识符引用其他聚合:进一步提高性能,并且解耦聚合间的持久化类型依赖

利用最终一致性更新其他聚合:不要求实时更新两个聚合的数据,关注的是最终一致性(区别于实时一致性)


聚合间的最终一致性是什么?

最终一致性不要求系统在完成操作后,立刻达到全局一致性。但会保证在一段时间后,达成全局一致性


聚合间的操作为什么使用的是最终一致性?

与最终一致性相对的,是实时一致性。如果想在跨聚合的操作中实现实时一致性,会大大增加系统的复杂性和耦合度。


实体

实体是业务逻辑的具象化描述,它代表了有唯一标识的业务概念。一个实体包含了这个业务的状态和行为,所以它是一个对业务的动态描述。


聚合和实体的关系

聚合的概念,比实体更大。聚合包含一个根实体,并且在聚合中,它是一组相关对象的集合,并且被作为修改数据的单元。

根实体:作为聚合对外交互的唯一通道,用于维护聚合内的规则和一致性。

实体作为聚合的一部分:包含一个或多个的实体和值对象,它们共同工作,维护一套业务逻辑或业务规则。

聚合保障了具体业务的一致性和原子性,而实体为这些业务操作提供了具体的实现。


避免在设计聚合时过度抽象,过度抽象会导致:

模型和领域专家心智模型不一致

形成错误的抽象,殃及 UI

在面对具体问题时,可能需要使用通用方法解决具体问题,导致形成复杂的层次结构

如果未来的需求和现有模型不一致,可能会带来更大的成本问题(改造、以及之前花费比原有更多的时间去做的抽象)


在使用增强领域事件时,注意不要传递太多的属性,否则可能会对消费者产生误导。

js
{
  data: {
    ceName: "xxx",
    payload: {
      ceName: "xxx",
    }
  }
}

假设在外层有个 ceName,payload 内层还有一个 ceName,业务上,它代表同一个意思,并且:

  • A 状态下,ceName 为空,payload.ceName 为正确值
  • B 状态下,ceName 为正确值,payload.ceName 为空
  • C 状态下,ceName 为正确值,payload.ceName 为错误值

这样不仅有冗余职责的同名属性,并且在不同位置来回横跳,还有可能出现错误值,这样接入方会十分混乱,并且出现异常。


触发领域事件的动作:

执行命令(通过 UI 触发,事件可能会被拒绝)

其他原因:特殊的时间节点(既成事实不可拒绝执行事件)


什么是事件风暴?

通过一系列方法,对业务进行分析建模,并形成基于时间流的命令/领域事件图:

  1. 按照时间线,从左到右罗列出关联的领域事件
  2. 将触发领域事件的命令,在每个领域事件左侧写上,形成一一对应的关系
  3. 基于所有的命令,分析如果一个命令需要多个领域事件,则抽象成一个类似于“事件组”的卡片,将“事件组”与一个命令关联起来,否则直接一对一连线关联即可。并且如果有非常重要的领域事件干系人,可以把干系人写在笔上
  4. 为领域事件划定边界,形成限界上下文,并且在限界上下文间的交互中,使用领域事件进行关联
  5. 识别用户操作所需要的视图,如必要则关联上干系人

有效建模的要素

  1. 模型和现实绑定
  2. 建立了一种基于模型的语言
  3. 开发一个蕴含丰富知识的模型
  4. 提炼模型
  5. 头脑风暴和实验

好的 service 的特点:

  • 是无状态的
  • 它的处理逻辑不应该是实体或值对象能包含的部分,应该是独立的一块公用逻辑
  • 需要基于其他元素定义

Factory 承担领域对象生命周期中的开始部分,而 Repository 负责承担领域对象生命周期的中间和结束部分。

|----开始----|----中间----|----结束----|

|--Factory--|-------Repository--------|