领域驱动设计-14章
第四部分 战略设计
随着系统的增长,它会变得越来越复杂,当我们无法通过分析对象来理解系统的时候,就需要掌握一些操纵和理解大模型的技术了。
三大主题:上下文、精炼和大比例结构。
上下文:模型必须在逻辑上保持整体的一致
精炼:减少混乱,并且把注意力集中到正确的地方
大比例结构:描述整个系统。responsibility layer(职责层)
14. 保持模型的完整性
模型的最基本需求是它应该保持内部的一致性、术语总具有相同的意义并且不包含互相矛盾的规则:尽管我们很少明确地考虑这些需求。模型的内部一致性又叫做"统一",这样每个术语都不会有模棱两可的意义,也不会有规则冲突。
大型系统领域模型的完全统一是不可行的,也不是一种经济有效的做法。
考虑在一个非常大的项目中努力把所有软件统一到一个模型中,但请一定要考虑下面的风险。
- 一次尝试对遗留系统做过多的替换
- 大项目可能会陷入困境,因为协调的开销太大,超出了这些项目的能力范围
- 具有一些特殊需求的应用程序可能不得不使用无法充分满足需求的模型,而只能将这些无法满足的行为放到其他地方
- 另一方面,试图用一个模型来满足所有人的需求可能会导致模型中包含过于复杂的选择,因而很难使用
此外,除了技术上的因素以外,权利上的划分和管理级别的不同也要求把模型分开。而且不同模型的出现也可能是团队组织和开发过程导致的结果。因此,即使完全的集成没有来自技术方面的阻力,项目也可能会面临多个模型。
通过预先决定什么应该统一,并实际认识到什么不能统一,我们就能改创建一个清晰地、、共同的视图。确定了这些之后,就可以着手开始工作,以保证那些需要统一的部分保持一致,不需要统一的部分不会引起混乱或破坏模型。
我们需要用一种方式来标记出不同模型之间的边界和关系。我们需要有意识地选择一种策略,并一致地遵守它。
bounded context(限界上下文)
context map(上下文图)
14.1 模式:限界上下文
细胞之所以会存在,是因为细胞膜定义了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜
任何一个大型项目都会存在多个模型,而当基于不同模型的代码被组合到一起后,软件就会出现bug,变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型不应该在哪个上下文中使用。
模型混乱的问题混在代码不能正常运行时暴露出来,但问题的根源却在于团队的组织方式和成员的交流方式。因此,为了澄清模型的上下文,我们既要注意项目,也要注意它的最终产品(代码、数据库模式等)
模型是软件系统中一个有界的部分,一个部分只应用一个模型,并尽可能地保持统一。团队组织中必须一致遵守这个定义。
因此:明确地定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)设置模型的边界。在这些边界中严格保持的一致性,而不要受到边界之外问题的干扰和混淆。
识别限界上下文中的不一致
很多症状都可能表明模型中出现了差异。最明显的症状是已编码的接口不匹配。一些细微的意外行为也可能是一种信号。采用了自动测试的持续集成可以帮助捕捉到这类问题。但语言上的混乱往往是一种早期的警告信号。
将不同模型的元素组合到一起可能会引发两类问题:重复的概念和假同源。
重复的概念是指两个模型元素(以及伴随的实现)实际上表示同一个概念。每当这个概念的信息发生变化时,都必须要更新两个地方。
假同源可能稍微少见一点,但它潜在的危害更大。它是指使用相同术语(或已实现的对象)的两个人认为他们是在谈论同一件事情,但实际上并不是这样。
说英语的人在学习西班牙语时常常会无用embarazada这个词。这个词的意思并不是embarrassed(难堪的),而是pregant(怀孕的)
14.2 模式:持续集成
定义完一个限界上下文后,必须让它保持合理化
开发一个统一系统(无论规模大小)需要维持很高的沟通水平,而这一点常常很难做到。我们需要通过运用各种方法来增进沟通并减小复杂性。还需要一些安全防护措施,以避免过于谨慎的行为。
持续集成是指把一个上下文中的所有工作足够频繁地合并到一起,并使它们经常保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。持续集成有两个级别的操作:1)模型概念的集成 2)实现的集成
团队成员之间通过经常沟通来保证概念的集成。团队必须对不断变化的模型形成一个共同的理解。有很多方法可以帮助做到这一点,但最基本的方法是对通用语言多加锤炼。同时,实际工件是通过系统性的合并/构建/测试过程来集成的,这样的过程能够尽早暴露出模型的分裂问题。用来集成的过程有很多,大部分有效的方法都具有以下这些特征:
- 分步集成,采用可重复使用的合并/构建技术
- 自动测试套件
- 有一些规则,用来为那些尚未集成的改动设置一个合理的、稍高的生命期上限
有效过程的另一面是概念集成,虽然它很少被正式地纳入进来
- 在讨论模型和应用程序时要坚持使用通用语言
- 在模型驱动设计中,概念集成为实现集成扫清了道路,而实现集成验证了模型的有效性和一致性,并暴露出模型分裂这个问题
- 严格坚持使用通用语言,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识
- 最后,不要在持续集成中做一些不必要的工作,持续集成只有在限界上下文中才是重要的
14.3 模式:上下文图
只有一个限界上下文并不能提供全局视图。其他模型的上下文可能仍不清楚而且还在不断变化。
其他团队中的人员并不是十分清楚上下文的边界,他们会不知不觉地做出一些更改,从而使边界变得模糊或者使互连变得复杂。当不同的上下文必须互相连接时,它们可能会互相重叠。
限界上下文之间的代码重用是很危险的,应该避免。功能和数据的集成必须要通过转换去实现。通过定义不同上下文之间的关系,并在项目中创建一个所有模型上下文的全局视图,可以减少混乱。
识别每个模型在项目中的作用,并定义其限界上下文。这包括非面向对象子系统的隐含模型。为每个限界上下文命名,并把名称添加到通用语言中。
描述模型之间的接触点,明确每次交流所需的转换,并突出任何共享的内容。
画出现有的范围,为稍后的转换做好准备。
如果你发现模型产生了分裂-模型完全混乱且包含不一致时,你该怎么办呢?这时一定要十分注意,先把描述工作停下来。然后,从精确的全局角度来解决这些混乱点。小的分裂可以修复,并且可以通过实施一些过程来为修复提供支持。如果一个关系很模糊,可以选择一种罪接近的模式,然后向此模式靠拢。最重要的任务是画出一个清晰地上下文图,而这可能意味着修复实际发现的问题。但不要因为修复必要的问题而重组整个结构。我们只需修改那些明显的矛盾即可,直到得出一个明确的上下文图,在这个图中,你的所有工作都被放到某个限界上下文中,而且所有互联的模型都有明确的关系。
一旦有了一致的上下文图,就会看到需要修改的那些地方。在经过深思熟路后,你可以修改团队的组织或设计。记住,在更改实际上完成以前,不要先修改上下文图。
与其他上下文和谐共存的一个秘诀是拥有有效的接口测试集。正如里根总统在裁减核武器谈判时所说的名言"信任,但要确认"
模型上下文总是存在的,但如果我们不注意的话,它们可能会发生重叠和变化。通过明确地定义限界上下文和上下文图,团队就可以掌握模型的统一过程,并把不同的模型连接起来。
14.3.1 测试上下文的边界
对各个限界上下文的接触点的测试特别重要。这些测试有助于解决转换时所存在的一些细微问题以及弥补边界沟通上存在的不足
14.3.2 上下文图的组织和文档化
1)限界上下文应该有名称,以便可以讨论它们。这些名称应该被添加到团队的通用语言中
2)每个人都应该知道边界在哪里,而且应该能够分辨出任何代码段的context,或任何情况的context
14.4 限界上下文之间的关系
把模型连接到一起之后,就能够把整个企业系统涵盖在内。这些模式有两个目的,一是为成功地组织开发工作设定目标,二是提供用于描述现有组织的术语。
另一方面,你可能会发现有关系很混乱或过于复杂。要想得到一个明确的上下文图,需要重新组织一些关系。在这种情况或任何需要考虑重组的情况下,这些模式提供了各种不同的选择。这些模式的主要区别包括你对另一个模型的控制程度、两个团队之间合作水平和合作类型以及特性和数据的集成程度。
共享内核(shared kernel)
客户/供应商关系(customer/supplier)
独立自主模式(separate way)
大多数项目都需要与遗留系统与外部系统进行一定程度的集成,这就需要使用open host service(开放主机服务)或anticorruption layer(防腐层)
14.5 模式:共享内核
当功能集成很有限时,持续集成的开销可能会变得非常高。尤其是当图案吨的技能水平或行政组织不能保持持续集成,或者只有一个庞大的、笨拙的团队时,更容易发生这种情况。在这种情况下可以定义单独二限界上下文,并组织多个团队。
当不同团队开发一些紧密相关的应用程序时,如果团队之间不进行协调,即使短时间内能够取得快速进展,他们开发出的产品也可能互相不适合。最后可能不得不在转换层上花费大量时间,而且得到的产品也"五花八门"
从领域模型中选出两个团队都同意共享的一个子集。当然,除了模型的这个子集以外,这还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的状态,而且一个团队在没与另一个团队商量的情况下不应擅自更改它。
功能系统要经常进行集成,但集成的频率应该比团队中持续集成的频率低一些。在进行这些集成的时候,两个团队都要进行测试。
共享内核通常是核心域,或是一组通用子领域,也可能二者兼有,它可以使两个团队都需要的任何一部分模型。使用共享内核的目的是减少重复(但并不能消除重复,因为只有在一个限界上下文中才能消除重复),并且使两个子系统之间的集成变得相对容易一些
14.6 客户/供应商关系
场景:一个子系统的主要任务是服务于另一个子系统,或者执行分析功能的"下游"组件向"上游"组件反馈的信息非常少,所有的依赖性都是单向的
上下游子系统很自然地分隔到两个限界上下文中
在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道对方的约定和进度
两个团队一起开发自动验收测试,用来验证预期的接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对下游团队产生副作用
在迭代期间,下游团队成员应该像传统的客户一样随时回答上游团队的提问,并帮助解决问题
这种模式有两个关键要素
1)关系必须是客户与供应商的关系,其中客户的需求是至关重要的。由于下游团队并不是唯一的客户,因此不同客户的要求必须通过协商来平衡,但这些要求都是非常重要的
2)必须有一个自动测试套件,使上游团队在修改代码时不必担心破坏下游团队的工作,并使下游团队能够专注于自己的工作,而不用总是密切注意着上游团队的行动
在接力赛中,前面的选手在接棒的时候不能一直回头看,这位选手必须相信队友能够把接力棒准确地交到他手中,否则整个团队的速度无疑会慢下来
运输案例:分析公司收到的所有预订,以便查看如何实现收益的最大化。团队成员可能发现货轮上还有空位置,并建议接收超订。他们可能发现货轮过早地装满了散装货物,从而使公司不得不拒绝利润更大的专门货物
预订系统(上游)->收益分析(下游)
14.7 模式:conformist(跟随者模式)
当两个开发团队具有上/下游关系时,如果上游团队没有动机满足下游团队的需求,那么下游团队将无能为力。出于利他主义的考虑,上游开发人员可能会做出承诺,但他们可能不会履行承诺。下游团队出于良好的意愿会相信这些承诺,从而根据一些永远不会实现的特性来指定计划。下游项目只能被搁置,直到团队最终学会利用现有条件自力更生为止。下游团队不会得到根据他们的需求而量身定做的接口。
在这种情况下,有3种可能的解决路径:
- 完全放弃对上游的利用。如果下游团队决定切断这条链,他们将走上separate way(独立自主)的道路
- 必须保持这种依赖性
- 上游的设计很难使用,下游必须开发其自己的模型,他们将完全负责开发一个转换层,这个层会非常复杂(防腐层)
- 上游设计的质量不是很差,可以使用跟随者(conformist)模式
跟随者模式类似于共享内核模式。这两种模式之间的区别在于决策指定和开发过程不同。共享内核是两个高度协调的团队之间的合作模式,而跟随者模式则是与一个对合作不感兴趣的团队进行集成
14.8 模式:防腐层(anticorruption layer)
新系统几乎总是需要与遗留系统或其他系统进行集成,这些系统具有其自己的模型。当把设计的很完善的限界上下文与合作团队的上下文连接时,转换层可能很简单,甚至很优雅。但是,当边界那侧发生渗透时,转换层就要承担起更多的防护职责
当正在构建的新系统与另一个系统的接口很大时,为了克服连接两个模型而带来的困难,新模型所表达的意图可能会被完全改变,最终导致它被修改得像是另一个系统的模型了(以一种特定的风格)。遗留系统的模型通常很弱。即使是一些例外的被开发得很好的模型,它们可能也不会符合当前项目的需要。然而,集成遗留系统仍然具有很大的价值,而且有时还是绝对必要的
正确的答案是不要全盘封杀与其他系统的集成。在我经历过的一些项目中,人们非常热衷于替换所有遗留系统,但由于工作量太大,这不可能立即完成。此外,与现有系统集成是一种有价值的重用形式。
创建一个隔离的层,以便根据客户自己的领域模型为客户提供相关的功能。这个层通过其现有接口与另一个系统进行对话,而只需对那个系统做出很少的修改,甚至无需修改。在内部,这个层在两个模型之间进行必要的双向转换。
防腐层并不是向另一个系统发生消息的机制。相反,它是在不同的模型和协议之间的转换概念对象和操作的机制。防腐层本身就可能是一段复杂的软件。
1. 设计防腐层的接口
防腐层的公共接口通常以一组service的形,式出现,但偶尔也会采用entity的形式。构建一个全新的层来负责两个系统的语义之间的转换为我们提供了一个机会,使我们能够重新对那个系统的行为进行抽象,并按照与我们的模型一致的方式把服务和信息提供给我们的系统。每个service都为我们的模型履行某种单一的职责。
2. 实现防腐层
对防腐层设计进行组织的一种方法是把它实现为facade(门面模式)、adapter(适配器模式)和转换器的组合,外加两个系统之间进行对话所需的通信和传输机制
门面模式应该于另一个系统的限界上下文,它只是为了满足你的专门需要而呈现出的一个更友好的外观
适配器模式是一个包装器,它允许客户使用另外一种协议,这种协议可以使行为实现者不理解的协议。当客户向适配器发送一条消息时,适配器将消息转换为一条在语义上等同的消息,并将其发送给"被适配者"(adaptee)。然后adapter对响应消息进行转换,并将其发回。
我们所定义的每种service都需要一个支持其接口的adapter,这个适配器还需要知道怎样才能向其他系统或其facade发出相应的请求。
剩下的要素就是转换器了。
14.9 模式:独立自主模式
我们必须严格划定需求的范围。如果两组功能之间的关系并非必不可少,那么两者完全可以彼此独立。
集成总是代价高昂,而有时获益却很少
一周内指定一个新的计划
整理需求列表,评估它们的难度和重要性
删减那些困难的和不重要的需求
为剩下的需求列表排列顺序
直到只有一个被证明是真正重要的
终于认识到有些特性几乎没有从集成得到任何好处
14.10 模式:开放主机服务
当一个子系统必须与大量其他系统进行集成时,为每个集成都定制一个转换层可能会减慢团队的工作速度。需要维护的东西会越来越多,而且进行修改的时候担心的事情也会越来越多
团队可能正在反复做着同样的事情。如果一个子系统有某种内聚性,那么或许可以把它描述为一组service,这组service满足了其他子系统的公共需求
要想设计出一个足够干净的协议,使之能够被多个团队理解和使用,是一件十分困难的事情,因此只有当子系统的资源可以被描述为一个内聚的service集并且必须很多集成的时候,才值得设计这样一种协议。在这些情况下,它能够把维护模式和持续开发区别开
因此:
定义一个协议,把你的子系统作为一组service供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转化器来补充协议,以便使共享协议简单且内聚。
14.11 模式:公开发布的语言
两个限界上下文之间的模型转换需要一种公共的语言。
与现有领域模型之间进行直接的转换可能不是一种好的解决方案。这些模型可能过于复杂或设计的较差。它们可能没有被很好地文档化。如果把其中的一个模型作为数据交换语言,它实质上就被固定住了,而无法满足新的开发需求。
案例:XML
把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换
14.12 大象的统一
盲人摸象
如果两个盲人都摸到象鼻子,一个认为是蛇,一个认为是消防水龙,那么他们将更难集成
事实上,他们需要一个新的抽象,这个抽象需要把蛇的"活着的特性"与消防水龙的喷水功能合并到一起,而这个抽象还应该排除先前两个模型中的一些不确切的含义,例如人们可能会想到的毒牙
尽管已经把部分合并成一个整体,但得到的模型还是很简陋的,它缺乏内聚性,也没有形成一个底层的领域模型。在持续精化的过程中,新的理解可能会产生更深刻的模型。新的应用程序需求也可能会促成产生更深刻的模型。如果大象开始移动了,那么"树"理论就站不住脚了,而盲人建模者们可能会有所突破,形成"腿"的概念。
模型集成的第二部是去掉各个模型中那些偶然或不正确的方面,并创建新的概念,本例中,这个概念就是一种"动物",每个部分都有其自己的属性以及与其他部分的明确关系。在很大程度上,成功的模型应该尽可能做到精简。
如果目标只是找到大象,那么只要对每个模型中所表示的位置进行转换就可以了。紧接着,通过新需求和进一步的理解集沟通的推动,模型可以得到加深和精化。
承认多个互相冲突的领域模型实际上正是面对现实的做法。通过定义每个模型都适用的上下文,可以维护每个模型的完整性,并清楚地看到要在两个模型创建的任何特殊接口的含义。盲人没办法看到整个大象,但只要他们承认各自的理解是不完整的,它们的问题就能得到解决。(我们也是盲人)
14.13 选择你的模型上下文策略
在任何时候,绘制出上下文图来反映当前状况都是很重要的。但是,当绘制好上下文图之后,你可能又非常想根据实际情况对它进行修改。现在,你可以开始有意识地选择上下文边界和关系。以下是一些指导原则。
1. 制定团队决策或更高层的决策
团队决定在哪里定义限界上下文,并且被团队里的每个人理解和一致同意
是否扩展或分割限界上下文,应该权衡团队独立工作的价值和能产生直接且丰富集成的价值,以这两种价值之间的成本-效益权衡作为决策的依据
2. 在上下文中工作
实际上,我们自己也是所工作的主要上下文的一部分
3. 转换边界
在画出限界上下文的边界时,有无数种情况,也有无数种选择。但通常是对下面所列出的各种因素进行权衡。
- 首选较大的限界上下文
- 当用一个统一模型来处理更多任务时,用户任务之间的流动更顺畅
- 一个内聚模型比两个不同模型更加它们之间的映射更容易理解
- 两个模型之间的转换会很难(有时甚至是不可能的)
- 共享语言可以使团队沟通起来更清楚
- 首选较小的限界上下文
- 开发人员之间的沟通开销减少了
- 由于团队和代码规模较小,持续集成更容易了
- 较大的上下文要求更加通用的抽象模型,而掌握所需技巧的人员会出现短缺
- 不同的模型可以满足一些特殊需求,或者是能够把一些特殊用户群的专门术语和通用语言的专门术语包括进来
4. 接受那些我们无法更改的事物:描述外部系统
5. 与外部系统的关系
这里可以应用三种模式。
首先,可以考虑独立自主模式。当然,如果你不需要集成,就不要把他们包括进来。集成要花费很大代价而且还会分散精力,因此要尽可能为你的项目减轻负担。
如果集成确实非常重要,可以在两种极端模式之间选择一种:跟随者模式或防腐层模式
6. 正在设计的系统
少于10人的团队:为正在设计中的整个设计使用一个限界上下文
团队规模扩大:共享内核模式,把几组相对独立的功能划分到不同的限界上下文中,使得在每个限界上下文中工作的人员少于10人。在这些限界上下文中,如果有两个上下文之间的所有依赖都是单向的,就可以建成为客户/供应商模式。如果两个团队思想截然不同,以致它们的建模工作总是发生矛盾。可以让它们的模型采用独立自主模式。在需要集成的地方,两个团队可以共同开发并维护一个转换层,把它作为唯一的持续集成点。这与同外部系统的集成正好相反,在外部集成中,一般由防腐层来起调节作用,而且从另一端得不到太多的支持。
7. 满足不同模型的特殊需要
当不需要集成或者集成相对有限时,就可以继续使用已经习惯的术语,以免破坏模型。但这也有其自己的代价和风险。
- 没有共同的语言,交流将会减少
- 集成开销会更高
- 随着相同业务活动和实体的不同模型的发展,工作会有一定的重复
但是,最大的风险就是在面对更改时不好做出判断
有时会出现一个深层次的模型,它把这些不同语言统一起来,并能够满足双方的要求。只有经过大量开发工作和知识消化之后,深层次模型才会在生命周期的后期出现。深层次模型不是计划出来的,我们只能在它出现的时候抓住机遇,修改自己的策略并进行重构。
8. 部署
9. 权衡
一般来说,我们需要在无缝功能集成的益处和额外的协调和沟通工作之间做出权衡。还要在更独立的操作与更顺畅的沟通之间做出权衡。
10. 当项目正在进行时
第一步:根据当前状况来定义限界上下文
第二步:围绕当前组织结构来加强团队的工作,把分散的转换代码重构到防腐层中,命名现有的限界上下文,并明确他们处于项目的通用语言中
第三步:考虑修改边界和它们的关系
14.14 转换
在很多情况下,我们必须改变最初有关边界以及限界上下文之间关系的决策,这是不可避免的
一般而言,分割上下文很容易,但合并他们或改变它们之间的关系却很难
1. 合并上下文:独立自主->共享核心
前提:翻译开销过高,重复现象很明显等
1)评估初始状况,在开始统一两个上下文之前,一定要确信它们确实需要统一
2)建立合并过程,测试
3)选择某个小的子领域作为开始
4)从两个团队中一共选出2~4位开发人员组成一个小组,由他们来为子领域开发一个共享的模型。不管模型是如何得出的,它的内容必须详细
5)来自两个团队的开发人员一起负责实现模型
6)每个团队的开发人员都承担与新的共享内核集成的任务
7)清除那些不再需要的翻译
在后序的项目迭代中,重复第3-7步共享更多内容
2.合并上下文:共享核心->持续集成
1-6步略
3. 逐步淘汰遗留系统
1-6步略
4. 开放主机服务->公开发布的语言
1-7步略