乐于分享
好东西不私藏

领域驱动设计-软件核心复杂性应对之道(14)_保持模型的完整性

领域驱动设计-软件核心复杂性应对之道(14)_保持模型的完整性
本章的核心观点是:在一个大型系统中,试图用一个统一的模型覆盖所有部分是不切实际且代价高昂的。 模型必然会出现分裂。关键任务不是试图阻止分裂,而是有意识地识别、划定并管理不同模型之间的边界和关系,从而在保持各自模型内聚性的同时,实现所需的集成。

核心梳理

1. 问题的根源:模型的分裂

* 当不同团队或不同功能模块基于隐含的、不一致的模型进行开发时,就会发生模型分裂。这导致术语混淆、规则冲突和难以修复的Bug。例如,两个团队对“Charge”对象有不同的理解,各自扩展后导致系统崩溃。

2. 根本模式:BOUNDED CONTEXT(限上下文)

* 定义:明确地划定一个模型适用的范围。在这个边界内,模型必须保持逻辑上的一致(统一),而边界之外则可以使用其他模型。
* 作用:
* 为团队提供清晰的工作范围。
* 为其他团队提供自由,他们不需要遵守一个不适用于他们的单一模型。
* 与MODULE的区别:MODULE是在一个上下文内部组织元素,而BOUNDED CONTEXT是定义不同模型的边界。它们是不同颗粒度的划分。

3. 模式:CONTINUOUS INTEGRATION(持续集成)

* 定义:在同一个BOUNDED CONTEXT内部,通过频繁的代码合并、构建和自动化测试,来尽早发现并修复模型的分裂问题。
* 本质:是维护单一BOUNDED CONTEXT内部模型完整性的过程。它包括概念集成(通过通用语言沟通)和实现集成(通过技术手段)。

4. 模式:CONTEXT MAP(上下文图)

* 定义:一个将所有相关的BOUNDED CONTEXT以及它们之间关系的全局视图画出来。
* 作用:
* 提供一个“地图”,帮助所有人理解系统的整体结构和集成点。
* 明确每个团队/子系统的责任范围。
* 识别并描述不同上下文之间存在的关系类型。

5. BOUNDED CONTEXT之间的关系模式

* SHARED KERNEL(共享内核):两个团队共享一小部分共有的模型和代码。这部分共享的内容必须经过双方同意,并且任何修改都要双方协调。这是一种高耦合、高协调的关系。
* CUSTOMER/SUPPLIER DEVELOPM ENT TEAM(客户/供应商开发团队):一个团队(上游/供应商)的模型依赖于另一个团队(下游/客户)的需求。上游团队与下游团队合作,制定并满足后者的需求。
* CONFORMIST(跟随者):下游团队严格遵循上游团队的模型,不做任何自己的模型扩展。这简化了集成,但牺牲了下游团队的模型自由度。
* ANTICORRUPTION LAYER(防损层):下游团队创建一个隔离层,将外部系统的模型“翻译”为自己的模型。这种转换保护了自身模型的纯洁性,使其不受外部模型污染。
* SEPARATE WAY(各行其道):当集成成本远超收益时,声明两个BOUNDED CONTEXT之间完全没有关联。这允许团队完全独立地开发。
* OPEN HOST SERVICE(开放主机服务):当一个子系统需要与许多其他系统集成时,它定义一组稳定的SERVICE(服务)和协议作为公共接口,供其他系统调用。
* PUBLISHED LANGUAGE(发布语言):定义了用一个良好文档化的、公共的交换语言(如XML Schema、JSON Schema)来进行模型之间的通信,它可以独立于任何一个参与集成的模型。

项目使用时的指导原则

基于以上核心思想,在项目中“保持模型完整性”时,可以遵循以下原则:

1. 第一步:用“CONTEXT MAP”画出现状,而非理想国 🗺️

* 原则:在有意识地改变它之前,首先要客观、诚实地描述现有的模型边界。不要试图画出一个你认为“应该如此”的理想地图。这是所有后续战略决策的基础。
* 行动:
* 绘制地图:在项目早期的白板或文档中,画出当前你认为存在的所有BOUNDED CONTEXT(子系统、模块、外部系统)。
* 命名上下文:为每个BOUNDED CONTEXT取一个简短、明确的名称,并将其加入团队的UBIQUITOUS LANGUAGE中。
* 描述关系:对于这些上下文之间已知的交互,尝试用本章介绍的关系模式(如SHARED KERNEL、CONFORMIST等)来描述它们。

2. 为每个明确的BOUNDED CONTEXT建立“持续集成” 💪

* 原则:一旦划定了BOUNDED CONTEXT的边界,最优先的任务是维护其内部的统一。这需要通过持续集成来实现。
* 行动:
* 自动构建与测试:确保该BOUNDED CONTEXT内的所有代码都可以自动构建、合并,并运行全面的自动化测试。
* 坚守通用语言:团队内部必须坚持使用为该上下文定义的UBIQUITOUS LANGUAGE,避免随意引入或篡改术语。
* 频率决定成败:集成频率越高(如每天多次),模型发生分裂的风险就越低。

3. 明智地选择“关系模式”:在“耦合度”与“自由度”间权衡  ⚖️

* 原则:没有一种“正确”的关系模式。关键是根据集成需求、团队协调能力、双方的控制权来做出理性的战略选择。
* 行动:
* 考虑集成需求:如果两个功能必须紧密协作,采用SHARED KERNEL或CUSTOMER/SUPPLIER;如果集成点很少,可以SEPARATE WAY。
* 评估团队协调能力:如果两个团队能紧密合作、频繁沟通,可以尝试SHARED KERNEL;如果团队之间沟通困难、权限分立,CONFORMIST或ANTICORRUPTION LAYER更合适。
* 权衡控制权:如果对上游系统(如遗留系统、外部系统)无法控制,只能做CONFORMIST或ANTICORRUPTION LAYER。如果你能控制它,可以考虑PUBLISHED LANGUAGE作为标准。

4. 优先选用“ANTICORRUPTION LAYER”来保护“核心域” 🛡️

* 原则:对于最值钱、最核心、最复杂的领域模型(CORE DOMAIN),要像保护“皇冠上的明珠”一样保护它。不要让它被外部系统的劣质模型(如遗留系统、第三方库)所侵蚀。最强大的保护工具就是ANTICORRUPTION LAYER。
* 行动:
* 隔离核心域:将核心域独立为一个BOUNDED CONTEXT。
* 创建翻译层:开发一个单独的组件或类,专门负责将外部系统的数据/请求翻译成核心域模型能理解的语言。
* 拒绝直接依赖:核心域中的任何代码都不应直接引用外部系统的类、库或服务。

5. 通过“PUBLISHED LANGUAGE”实现多系统的标准化集成 🌐

* 原则:当存在两个以上的系统需要相互通信时,最好不要让它们两两之间都建立复杂的转换层。设计一个共享的、稳定的通信语言。
* 行动:
* 定义交换语言:定义一套XML、JSON或其他格式的数据交换标准,这套标准独立于任何一个参与系统的内部模型。
* 转换为事件:核心模型内部的事件(如"OrderPlaced"、"PaymentReceived")在需要通知外部系统时,先被发布为该公开语言格式的事件(如"OrderPlacedEvent"、"PaymentReceivedEvent")。
* 转换为命令:来自外部的请求,如"POST /products",其数据格式也是公开语言,然后由接收系统内部的ANTICORRUPTION LAYER将其转换为内部模型能理解的命令。
总结: 在项目中运用“保持模型完整性”的理念,核心是有意识地接受并管理模型的多样性。其指导原则是:首先绘制客观的现状地图,并在此基础上做出战略选择;在每一个BOUNDED CONTEXT内通过持续集成维护其统一性;根据集成需求与协调成本,明智地选择“共享内核”、“跟随者”、“防损层”等关系模式;并优先使用防护层来保护核心域,使用发布语言来应对多系统集成的复杂性。