常见的DDD设计中的经验教训!
@TOC
推荐超级课程:
在技术领域工作中,我犯过无数设计错误。但我发现,在领域驱动设计(DDD)中犯的错误特别难以原谅。在DDD中错误的抽象比其他设计方法更具破坏性的影响。 在本文中,我分享了DDD中最昂贵的设计错误;一个普遍的错误导致了一个紧密耦合的系统的存在。
背景
我在不同组织中遇到了许多臃肿和脆弱的客户API。当这种脆弱性的解决方案最终变得太繁琐时,提出的解决方案是将客户API拆分为更小、目的驱动的服务。 由于企业存在是为了服务客户,将客户作为一个API太模糊且懒惰了。实际上,缺乏具体性是导致这种臃肿和脆弱的原因,您可以在其他示例中轻易看到,比如订单API,产品API,账户API等等。
我们打算在大多数情况下建模的是围绕这些概念的业务流程,而不是概念本身。 这就导致了我们的常见错误。
常见错误
将中心概念误解为边界上下文
“中心概念”是特定行业的一个关键观念,比如:
- 在银行业中的账户。
- 在保险业中的保单。
- 在供应链管理中的产品。
- 在航空公司中的预订。
- 在电子商务中的订单。
- 在电子商务中的客户。
“边界上下文 ”是业务领域中适用某些流程和规则的领域。这是中心概念在不同规则集下反复出现并合理展示的地方,比如:
- 银行业中的账户在贷款发起 vs. 计费 vs. 债务收款 vs. 营销与沟通中有所不同。
- 保险业中的保单在核保 vs. 索赔 vs. 检查中有所不同。
- 供应链管理中的产品在规划 vs. 采购 vs. 库存管理 vs. 交付中有所不同。
- 航空公司中的预订在预订 vs. 运营 vs. 货运管理 vs. 忠诚计划中有所不同。
- 电子商务中的订单在购买 vs. 供应链 vs. 履约 vs. 客户支持中有所不同。
- 电子商务中的客户在广告 vs. 订购 vs. 发货中有所不同。
这就是问题所在。由于它们的复杂性以及它们经常出现的频率,DDD中的中心概念比我们意识到的更容易绊倒我们。它们欺骗我们,让我们误以为它们是真正的边界上下文。
让我们专注于银行业,那里我第一次经历了这个错误。
我们决定构建一个账户API——乍一看似乎是一个合理的想法。但是,账户不应该是一个边界上下文或API。我将从两个不同的角度论述理由;一个是理论角度,一个是实践角度。
理论观点
首先,声称对账户进行单一视角违背了DDD的核心原则,因为这将导致一个不灵活、臃肿的领域模型 。
不同的边界上下文通过独特的视角看待账户,而且在某些情况下,账户甚至可能在业务中被称为其他名字(普遍语言 )。
举个例子,在我们的示例中,以下是边界上下文与它们关注的账户信息:
- 贷款发起侧重于背景调查、风险评估和账户条款,例如利率和信用额度。
- 计费关注于及时付款和为账户生成对账单。
- 营销与沟通侧重于客户沟通偏好和为账户发送付款提醒。
- 债务追缴通过减少利率或为账户提供备选付款计划来帮助处于困境的客户。在某些不幸的情况下,他们可能将账户出售给催收机构——那些骚扰客户并追讨其资产的公司。
第二,DDD是关于解决复杂的业务问题,在那里软件模型应该反映企业流程。管理账户偏好是一个业务流程。债务追缴是一个业务流程。但是账户不是。它是一个存在于不同业务流程中的概念。
实践角度
将账户视为边界上下文会导致负面连锁后果。
不必要的耦合
第一点是账户边界上下文变成了一个“上帝域”,承担了比该承担的更多职责。结果,这会导致与其他边界上下文之间的紧密耦合。
团队摩擦
这种耦合对围绕问题组织团队产生负面影响。所有团队必须与账户互动以进行更改,这增加了依赖性和摩擦。这导致以下不良副作用之一:
- 账户团队被来自其他团队的功能请求压垮,随着请求数量的增加,造成延迟。
- 账户团队使其他团队能够提交关于他们的更改的拉取请求。这种方法仍然会引起延迟,因为账户团队要审查大量请求。
- 账户团队规模增加以匹配工作量,但团队成员开始更频繁地出现合并冲突。
遗憾的是,上面概述的所有方法都妨碍了团队的自治和软件发布的可预测性。结果,组织在同时执行许多想法时受到限制。
单点故障和弹性问题
由于其他边界上下文请求来自(或传播到)账户,跨域交互变得繁琐。这导致性能和弹性问题。
以账户作为真相来源后,它也成为整个系统的单点故障。例如,如果账户发起代码中的一个错误导致内存泄漏,它可能会影响与支付计划等无关的领域服务。这意味着不仅账户创建受到影响,而且另一个关键的业务功能——处理付款——也受到影响。
分解与封装
在审视我的经验时,我意识到其中很大一部分是用于系统的分解和封装。
分解是将问题分解为更小的部分。这些部分通常类似于必须进行范围确定的业务规则。这就是封装的作用。
封装是定义业务规则的边界,同时保护它们不泄露给其他区域。它定义其他系统如何访问这些规则并与其交互。
DDD也不例外。有了我们现在对于一个账户在不同情况下有多个表现的理解,我们应该评估它是否可以被分解为其他边界上下文。
接下来要考虑的是如何封装这些业务规则,使其得到界定并受边界上下文保护。换句话说,我们应该思考如何精确地表示中心概念,账户。
这就是聚合 的作用。聚合是我们账户的封装单元。它们负责根据业务流程维持数据的完整性。它们可以通过适用于各自边界上下文的API定义访问规则。
以下是我们如何将账户解散为不同边界上下文中的聚合的示例:
将账户边界上下文解散为聚合(收集账户、账户偏好、账户条款和账户支付)
对于债务追缴,账户的概念被称为收集账户,以示其与良好账户的不同状态。如果您需要授予特定的支付计划(不带利率的情况),则必须通过收集账户进行启用。
在计费中,账户支付是核心概念(聚合)。让客户进行付款、查看对账单和付款历史是该边界上下文特定操作的示例。
从实施角度来看,您应该期望债务追缴中的收集账户与计费中的账户支付有不同的模式和行为集。这是完全正常的,实际上,DDD原则鼓励这种行为。
总结
领域驱动设计是关于建模复杂的业务流程和规则的。DDD中常见的错误是将中心概念——比如金融业务中的“账户”、电子商务中的“客户”和保险业中的“保单”——错误地建模为边界上下文或API。这种错误在使用领域驱动设计时非常昂贵。 避免这种陷阱的关键是将设计与被建模的业务流程联系起来。这些中心概念确实在领域驱动设计中起着关键作用。但是它们的作用是作为Bounded Context的维护者和控制者(也就是:聚合),而不是Bounded Context本身。