最常见领域驱动设计错误

DDD中的错误抽象比其他设计方法具有更大的破坏性影响。这篇文章分享了 DDD 中代价最高的设计错误;导致单一和紧密耦合系统盛行的一个常见错误。

背景
企业中存在很多臃肿而脆弱的客户应用程序接口,而针对这种脆弱性提出的解决方案,最终会在客户应用程序接口不可避免地变得过于繁琐时,将其拆分成更小的、目的明确的服务。

既然企业是为客户服务而存在的,那么将 Customer 作为 API 来处理就显得过于模糊和懒惰了。事实上,缺乏具体性正是导致这种臃肿和脆弱的原因,这一点您可以从订单 API、产品 API、账户 API 等其他示例中轻易看出。

在大多数情况下,我们要建模的是围绕这些概念的业务流程,而不是概念本身。这就是我们常犯的错误。

将中心概念误认为是BC限制上下文
中心概念 是某一特定行业的关键理念,例如:

  • 银行业的账户。
  • 保险业的保单
  • 供应链管理中的产品
  • 航空公司的预订
  • 电子商务中的订单
  • 电子商务中的客户

”限制/限界/有界上下文(BC:Bounded Context)”是应用某些流程和规则的业务领域,这是中心概念在对每个限界上下文有意义的不同规则集下重复出现的地方, 例如:

  • 银行这个BC中账户与贷款发放、账单、债务催收、营销和传播等BC中账户不同。
  • 保险BC中的保单看起来与其他BC如承保、索赔和检查不同。
  • 供应链管理这个BC中的产品看起来与计划、采购、库存管理和交付等BC不同。
  • 航空公司的预订看起来与预订、运营、货运管理和忠诚度有所不同。
  • 电子商务中的订单 与采购、供应链、履行和客户支持看起来有所不同
  • 电子商务中的客户 看起来与广告、订购和运输不同。

这就是问题所在。由于它们的复杂性和出现的频率,DDD 中的中心概念比我们意识到的更让我们困惑。它们欺骗我们将它们视为真正的限界上下文。

例如构建一个Account API:

理论视角
首先,对账户采用单一视角有违DD 的核心原则,因为这会导致领域模型不灵活、臃肿。

不同的 "限界上下文BC "以独特的视角看待账户,在某些情况下,账户甚至可能在整个业务中被称为其他名称(泛在语言UL)。

为了说明问题,下面列出了我们示例中的BC有界上下文及其关注的账户信息:

  • 贷款发放侧重于背景调查、风险评估以及利率和信用额度等账户条款。
  • 计费关注账户的及时支付和对账单生成。
  • 市场营销和沟通侧重于客户沟通偏好和账户付款提醒。
  • 债务催收通过降低利率或提供其他账户付款计划来帮助有困难的客户。在某些不幸的情况下,他们可能会将账户出售给讨债公司--那些骚扰客户并追讨其资产的公司。

其次,DDD 是为了解决复杂的业务问题,软件模型应反映业务流程。管理账户偏好是一个业务流程。收账是一个业务流程。但账户不是。它是一个存在于不同业务流程中的概念。

实际角度
将账户视为一个BC限制上下文会导致负面的连带后果。

1、不必要的耦合
首先,账户这个BC限制上下文变成了 "上帝领域",承担了比它应该承担的更多责任。因此,这就造成了与其他BC有界上下文的紧密耦合。

2、团队摩擦
这种耦合会对围绕问题组织团队产生负面影响。所有团队都必须与账户互动才能做出改变,这就增加了依赖性和摩擦。这会导致以下不良副作用之一:

  • 账户团队超负荷处理来自其他团队的功能请求,随着请求数量的增加而导致延迟。
  • 账户团队让其他团队为其变更提交拉取请求。这种方法仍然会造成延迟,因为账户团队要审查大量请求。
  • 账户团队的规模随着工作量的增加而扩大,但团队成员开始经常出现合并冲突。

遗憾的是,上述所有方法都妨碍了团队的自主性和软件发布的可预测性。因此,组织在同时执行许多想法时会受到限制或限定。

3、单点故障和弹性问题
由于其他BC有界上下文会向账户请求信息(或向账户传播信息),跨域交互就会变得非常频繁。这将导致性能和弹性问题。

账户作为信息源,也会成为整个系统的单点故障。例如,如果账户创建代码中的一个错误导致内存泄漏,就会中断支付调度等无关领域的服务。这意味着不仅账户创建会受到影响,另一个关键业务功能--处理付款--也会受到影响。

避免陷阱
1、分解与封装
当我对自己的经验进行评估时,我意识到其中很大一部分都用在了系统的分解和封装上。

  • 分解是将问题分解成更小的块。这些小块通常类似于必须确定范围的业务规则。这就是封装的作用所在。
  • 封装就是定义业务规则的边界,同时保护这些规则不会泄漏到其他领域。它定义了其他系统如何访问这些规则并与之交互。

DDD 也不例外。我们现在知道,一个账户在不同情况下有多种表征,因此我们应该评估它是否可以分解成其他BC限制上下文?

下一步是考虑如何封装业务规则,使其在BC有界上下文中受到范围限制和保护。换句话说,我们应该考虑如何精确地表示中心概念 "账户"。

这就是聚合的作用所在:聚合体是账户的封装单元。它们负责根据业务流程维护数据的完整性。它们可以在自己的 "BC有界上下文"中通过合适的应用程序接口(API)定义访问规则。

下面是一个例子,说明我们如何在不同的 "BC有界上下文 "中将 "账户 "分解为 "聚合":

  • 在债务催收BC中,账户的概念被称为 "催收账户"(CollectionAccount),以表示其不同于良好账户的地位。如果需要授予一个特定的付款计划(比如不计利息),就必须通过 CollectionAccount 来进行更改。
  • 在账单BC下,账户付款是中心概念(DDD聚合)。使客户能够付款、查看对账单和付款历史记录是该 "BC有界上下文 "的特定操作示例。

从实现的角度来看,您应该期望债务催收BC中的 CollectionAccount 与账单BC中的 AccountPayments 有不同的模式和行为集。这完全正常,事实上,DDD 原则也鼓励这样做。

结束语
领域驱动设计是对复杂的业务流程和规则进行建模。领域驱动设计中的一个常见错误是将中心概念(如金融服务中的 "账户"、电子商务中的 "客户 "和保险中的 "保单")错误地建模为BC有界上下文或应用程序接口。要扭转这种错误,尤其是在使用领域驱动设计时,代价相当高昂。

要避免这一陷阱,关键在于将设计与建模的业务流程联系起来。这些中心概念在领域驱动设计中确实起着至关重要的作用。但它们的作用是作为BC有界上下文整体一致性的维护者和控制者(又名:DDD聚合),而不是作为BC有界上下文本身。

什么是BC:Bounded Context
"bounded" 在英文中可以表示限制、限定和限界,但它们的语境略有不同。

  • 限制 (restriction): 当我们说某物或某活动有界限或受到限制时,可以使用 "bounded"。
    • "The government has set bounded restrictions on travel."
    • "这项政策对旅行设置了限制。"
  • 限定 (limitation): 有时 "bounded" 可以表示对某事物进行了限定或确定范围,但这通常侧重于界定一种特定的范围或条件。
    • "The time for completing the project is bounded to six months."
    • "完成项目的时间被限定在六个月内。"
  • 限界 (bound): 这个词也可以表示一种限制,但更多地侧重于某物的边界或范围。
    • "The discussion was bounded by time constraints."
    • "讨论受到时间限制。"

"限制"、"限定"、"限界"和"受限"这四个词在某些情况下可以表达类似的意思,但它们在用法和含义上有一些细微的区别。

  • 限制 (xiànzhì): 指对某事物的范围、数量、活动等做出约束或限定,以防止其超出一定的界限。它通常暗示一种更为严格和强制性的约束。比如:
    • "政府制定了交通限制,禁止在市中心区域停车。"
    • "这项法律对公司的活动做出了严格的限制。"
  • 限定 (xiàndìng): 通常指明确规定某事物的特定条件或范围,但并不一定暗示强制性的限制。它可能更偏向于在某种程度上定义范围或条件。比如:
    • "这份契约对员工的加班进行了限定,每周不超过20小时。"
    • "这个方案限定了项目的时间框架,必须在六个月内完成。"
  • 限界 (xiànjiè): 指的是某事物的边界或范围,在这个边界内活动或表现。它通常用于描述某个领域的界限。比如:
    • "这个概念的限界不仅限于科学领域,还涉及哲学和文化。"
    • "这项计划的限界是确保在预算内完成项目。"
  • 受限 (shòuxiàn): 指的是受到限制或约束的状态。它暗示某个实体或行为受到某种程度的限制。比如:
    • "由于资源受限,我们必须精打细算地运用它们。"
    • "这项政策使得公司的发展受到了一定程度的限制。"

总的来说,这些词在一些情境下可以互换使用,但在特定的语境中,它们可能会有细微的差别,比如表达的强调、涵义的范围等。