第14章 保持模型的完整性

我曾经参加过一个项目,在这个项目中几个团队同时开发一个重要的新系统。有一天,当负责“客户发票”模块的团队正准备实现一个他们称之为Charge(收费)的对象时,他们发现另一个团队已经构建了这个对象,于是决定重复使用这个现有对象。他们发现它没有expense code(费用代码)属性,因此添加了一个。对象中有一个posted amount(过账金额)属性是他们所需要的。他们本来计划把这个属性叫做amount due(到期金额),但名称不同有什么关系呢?于是他们把名称改成了“posted amount”。又添加了几个方法和关联后,他们得到了所需的对象,而且没有扰乱任何事情。虽然他们必须忽略掉一些不需要的关联,但他们的模块运行很正常。

几天之后,“账单支付”模块出现了一些奇怪的问题(Charge对象最初就是为这个模块编写的)。系统中出现了一些奇怪的Charge,没有人记得曾经输入过它们,而且它们也没有任何意义。当使用某些函数时,特别是使用当月月初至今(month-to-date)的税务报表时,程序就会崩溃。调查发现,当用于计算所有当月付款的可扣除总额的函数被调用时,程序就会崩溃。那些来历不明的记录在percent deductible(可扣除百分比)字段中没有值,尽管数据录入应用程序的验证需要这个值,甚至为它设臵了一个默认值。

问题在于这两个团队使用了不同的模型,而他们并没有认识到这一点,也没有用于检测这一问题的过程。每个团队都对Charge对象的特性做了一些假设,使之能够在自己的上下文中使用(一个是向客户收费,另一个是向供应商付款)。当他们的代码被组合到一起而没有消除这些矛盾时,结果就产生了不可靠的软件。

如果他们一开始就意识到这一点,就能决定如何来解决它。他们可以共同开发出一个公共的模型,然后编写自动测试套件来防止以后出现意外。也可以双方商定开发各自的模型,而互相不干扰对方的代码。无论采用哪种方法,首先都要明确边界,各模型只在各自的边界内使用。

他们在知道了问题所在之后采取了什么措施呢?他们创建了两个不同的类:Customer Charge (客户收费)类和Supplier Charge(供应商收费)类。并根据各自的需求定义了每个类。解决了眼前这个问题之后,他们又按以前的方式开始工作了。

模型最基本的要求是它应该保持内部一致,术语总具有相同的意义,并且不包含互相矛盾的规则:虽然我们很少明确地考虑这些要求。模型的内部一致性又叫做统一(unification),这种情况下,每个术语都不会有模棱两可的意义,也不会有规则冲突。除非模型在逻辑上是一致的,否则它就没有意义。在理想世界中,我们可以得到涵盖整个企业领域的单一模型。这个模型将是统一的,没有任何相互矛盾或相互重叠的术语定义。每个有关领域的逻辑声明都是一致的。

但大型系统开发并非如此理想。在整个企业系统中保持这种水平的统一是一件得不偿失的事情。在系统的各个不同部分中开发多个模型是很有必要的,但我们必须慎重地选择系统的哪些部分可以分开,以及它们之间是什么关系。我们需要用一些方法来保持模型关键部分的高度统一。所有这些都不会自行发生,而且光有良好的意愿也是没用的。它只有通过有意识的设计决策和建立特定过程才能实现。大型系统领域模型的完全统一即不可行,也不划算。

有时人们会反对这一点。大多数人都看到了多个模型的代价:它们限制了集成,并且使沟通变得很麻烦。更重要的是,多个模型看上去似乎不够雅致。有时,对多个模型的抵触会导致“极富雄心”的尝试——将一个大型项目中的所有软件统一到单一模型中。我自己就很后悔曾经这么做过了头。但请一定要考虑下面的风险。

(1) 一次尝试对遗留系统做过多的替换。

(2) 大项目可能会陷入困境,因为协调的开销太大,超出了这些项目的能力范围。

(3) 具有特殊需求的应用程序可能不得不使用无法充分满足需求的模型,而只能将这些无法满足的行为放到其他地方。

(4) 另一方面,试图用一个模型来满足所有人的需求可能会导致模型中包含过于复杂的选择,因而很难使用。

此外,除了技术上的因素以外,权力上的划分和管理级别的不同也可能要求把模型分开。而且不同模型的出现也可能是团队组织和开发过程导致的结果。因此,即使完全的集成没有来自技术方面的阻力,项目也可能会面临多个模型。

既然无法维护一个涵盖整个企业的统一模型,那就不要再受到这种思路的限制。通过预先决定什么应该统一,并实际认识到什么不能统一,我们就能够创建一个清晰的、共同的视图。确定了这些之后,就可以着手开始工作,以保证那些需要统一的部分保持一致,不需要统一的部分不会引起混乱或破坏模型。

我们需要用一种方式来标记出不同模型之间的边界和关系。我们需要有意识地选择一种策略,并一致地遵守它。

本章将介绍一些用于识别、沟通和选择模型边界及关系的技术。讨论首先从描绘项目当前的范围开始。BOUNDED CONTEXT(限界上下文)定义了每个模型的应用范围,而CONTEXT MAP(上下文图)则给出了项目上下文以及它们之间关系的总体视图。这些降低模糊性的技术能够使项目更好地进行,但仅仅有它们还是不够的。一旦确立了CONTEXT的边界之后,仍需要持续集成这种过程,它能够使模型保持统一。

其后,在这个稳定的基础之上,我们就可以开始实施那些在界定和关联CONTEXT方面更有效的策略了—从通过共享内核(SHARED KERNEL)来紧密关联上下文,到那些各行其道(SEPARATE WAYS)地进行松散耦合的模型。

第14章 保持模型的完整性 - 图1 图14-1 模型完整性模式的导航图

14.1 模式:BOUNDED CONTEXT

第14章 保持模型的完整性 - 图2

细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜

大型项目上会有多个模型共存,在很多情况下这没什么问题。不同的模型应用于不同的上下文中。例如,你可能必须将你的新软件与一个外部系统集成,而你的团队对这个外部系统没有控制权。在这种情况下,任何人都明白这个外部系统是一种完全不同的上下文,不适用他们正在开发的模型,但还有很多情况是比较含糊和混乱的。在本章开篇所讲的那个故事中,两个团队为同一个新系统开发不同的功能。那么他们使用的是同一个模型吗?他们的意图是至少共享其所做的一部分工作,但却没有界限告诉他们共享了什么、没有共享什么。而且他们也没有一个过程来维持共享模型,或快速检测模型是否有分歧。他们只是在系统行为突然变得不可预测时才意识到他们之间产生了分歧。

即使在同一个团队中,也可能会出现多个模型。团队的沟通可能会不畅,导致对模型的理解产生难以捉摸的冲突。原先的代码往往反映的是早先的模型概念,而这些概念与当前模型有着微妙的差别。

每个人都知道两个系统的数据格式是不同的,因此需要进行数据转换,但这只是问题的表面。问题的根本在于两个系统所使用的模型不同。当这种差异不是来自外部系统,而是发生在同一个系统中时,它将更难发现。然而,所有大型团队项目都会发生这种情况。

任何大型项目都会存在多个模型。而当基于不同模型的代码被组合到一起后,软件就会出现bug、变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型不应该在哪个上下文中使用。

模型混乱的问题最终会在代码不能正常运行时暴露出来,但问题的根源却在于团队的组织方式和成员的交流方法。因此,为了澄清模型的上下文,我们既要注意项目,也要注意它的最终产品(代码、数据库模式等)。

一个模型只在一个上下文中使用。这个上下文可以是代码的一个特定部分,也可以是某个特定团队的工作。如果模型是在一次头脑风暴会议中得到的,那么这个模型的上下文可能仅限于那次讨论。就拿本书中的例子来说,示例中所使用的模型的上下文就是那个示例所在的小节以及任何相关的后续讨论。模型上下文是为了保证该模型中的术语具有特定意义而必须要应用的一组条件。

为了解决多个模型的问题,我们需要明确地定义模型的范围——模型的范围是软件系统中一个有界的部分,这部分只应用一个模型,并尽可能使其保持统一。团队组织中必须一致遵守这个定义。

因此:

明确地定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设臵模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。

BOUNDED CONTEXT明确地限定了模型的应用范围,以便让团队成员对什么应该保持一致以及上下文之间如何关联有一个明确和共同的理解。在CONTEXT中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。在其他CONTEXT中,会使用其他模型,这些模型具有不同的术语、概念、规则和UBIQUITOUS LANGUAGE的技术行话。通过划定明确的边界,可以使模型保持纯粹,因而在它所适用的CONTEXT中更有效。同时,也避免了将注意力切换到其他CONTEXT时引起的混淆。跨边界的集成必然需要进行一些转换,但我们可以清楚地分析这些转换。

BOUNDED CONTEXT不是MODULE

有时这两个概念易引起混淆,但它们是具有不同动机的不同模式。确实,当两组对象组成两个不同模型时,人们几乎总是把它们放在不同的MODULE中。这样做的确提供了不同的命名空间(对不同的CONTEXT很重要)和一些划分方法。

但人们也会在一个模型中用MODULE来组织元素,它们不一定要表达划分CONTEXT的意图。MODULE在BOUNDED CONTEXT内部创建的独立命名空间实际上使人们很难发现意外产生的模型分裂。

示例 预订系统的上下文

一家运输公司的内部项目——为货物预订开发一个新的应用程序。这个应用由一个对象模型驱动。那么这个模型所应用的BOUNDED CONTEXT是什么呢?为了回答这个问题,我们必须看一下项目正在发生的事情。记住,这里是观察项目的现状,而不是它的理想状态。

预订应用程序的开发工作由一个项目团队负责。他们不能修改模型对象,但他们所构建的应用程序还必须要显示和操作这些对象。这个团队是模型的使用者。模型在应用程序(模型的主要使用者)中是有效的,因此预订应用程序在BOUNDED CONTEXT的边界之内。

已完成的预订必须传递给用于货物跟踪的遗留系统来处理。项目一开始就已决定新模型将与原有系统的模型不同,因此原来的货物跟踪系统位于BOUNDED CONTEXT的边界之外。新旧模型之间的必要转换由原有系统的维护团队来负责处理。转换机制不是由新模型驱动的。因此它不在BOUNDED CONTEXT中(转换其实是边界本身的一部分,这一点将在CONTEXT MAP中讨论)。将转换机制臵于CONTEXT之外(不基于模型),这一点很好。要求遗留系统的团队使用这个模型是不切实际的,因为他们的主要工作都发生在CONTEXT之外。

每个对象的整个生命周期都由负责模型的团队来处理,包括对象的持久化。由于这个团队也控制着数据库模式,因此他们特意把对象—关系映射设计得简单直接。换言之,数据库模式是由新模型驱动的,因此在BOUNDED CONTEXT的边界之内。

另有一个团队正在开发安排货轮航次的模型和应用。从项目一开始,这个团队与负责货物预订的团队就在一起工作,他们都打算开发一个单独的、统一的系统。这两支团队偶尔互相协调,也偶尔共享对象,但没有系统性地去做。他们不在同一个BOUNDED CONTEXT中工作。这会带来风险,因为他们并没有意识到各自正在使用不同的模型。到了集成的时候,就会出现问题,除非他们采取特定的过程来管理这种情况(共享内核可能就是一个很好的选择,本章后面会介绍)。但是,第一步是认清现状。他们不在同一个CONTEXT中,因此应该停止共享代码,直到做出一些改变之后再去共享。

在这个系统中,由该具体模型驱动的所有方方面面构成了其对应的BOUNDED CONTEXT,这包括模型对象、用于模型对象持久化的数据库模式以及预订应用程序。在这个CONTEXT中主要有两支团队在工作,一个是建模团队,另一个是应用程序团队。这个系统需要与遗留的货物跟踪系统交换信息,遗留系统的维护团队主要负责在这个边界上的转换,并且与建模团队进行合作。预订模型和航次安排模型之间没有明确定义的关系,定义这种关系应该是这两个团队的首要任务之一。同时,他们应该在共享代码或数据方面格外谨慎。

因此,通过定义这个BOUNDED CONTEXT,最终得到了什么?对CONTEXT内的团队而言:清晰!。这两支团队知道他们必须与这个模型保持一致。他们根据这一点制定设计决策,并注意防范出现不一致的情况。而CONTEXT之外的团队获得了:自由。他们不必行走在灰色地带,不必使用同一个模型,虽然他们还是总觉得应该使用同一个模型。但在这个具体例子中,最实际的收获是认识到了在预订模型团队和航次安排团队之间进行信息共享存在着风险。为了避免问题产生,他们实际上需要在共享的代价和收益之间作出权衡,并制定流程来确保其发挥作用。只有每个人都理解模型上下文的边界在哪里,这一切才会发生。

当然,边界只不过是一些特殊的位臵。各个BOUNDED CONTEXT之间的关系需要我们仔细地处理。CONTEXT MAP画出了上下文的范围,并给出了CONTEXT以及它们之间联系的总体视图,而几种模式定义了CONTEXT之间的各种关系的性质。CONTINUOUS INTEGRATION的过程可以使模型在BOUNDED CONTEXT中保持统一。

但在讨论所有这些模式之前,想一想当模型的统一性被破坏时,模型会是什么样子呢?我们又该如何识别概念上的不一致呢?

识别BOUNDED CONTEXT中的不一致

很多征兆都可能表明模型中出现了差异。最明显的是已编码的接口不匹配。对于更微妙的情况,一些意外行为也可能是一种信号。采用了自动测试的CONTINUOUS INTEGRATION可以帮助捕捉到这类问题。但语言上的混乱往往是一种早期的警告信号。

将不同模型的元素组合到一起可能会引发两类问题:重复的概念和假同源。重复的概念是指两个模型元素(以及伴随的实现)实际上表示同一个概念。每当这个概念的信息发生变化时,都必须更新两个地方。每次由于新知识导致一个对象被修改时,必须重新分析和修改另一个对象。如果不进行实际的重新分析,结果就会出现同一概念的两个版本,它们遵守不同的规则,甚至有不同的数据。更严重的是,团队成员必须学习做同一件事情的两种方法,以及保持这两种方法同步的各种方式。

假同源可能稍微少见一点,但它潜在的危害更大。它是指使用相同术语(或已实现的对象)的两个人认为他们是在谈论同一件事情,但实际上并不是这样。本章开头的示例就是一个典型的例子(两个不同的业务活动都叫做Charge)。但是,当两个定义都与同一个领域方面相关,而只是在概念上稍有区别时,这种冲突更难以发现。假同源会导致开发团队互相干扰对方的代码,也可能导致数据库中含有奇怪的矛盾,还会引起团队沟通的混淆。假同源这个术语在自然语言中也经常使用。例如,说英语的人在学习西班牙语时常常会误用embarazada这个词。这个词的意思并不是embarrassed(难堪的),而是pregnant(怀孕的)。很惊讶吧!

当发现这些问题时,团队必须要做出相应的决定。可能需要将模型重新整合为一体,并加强用来预防模型分裂的过程。分裂也有可能是由分组造成的,一些小组出于合理的原因,需要以一些不同的方式来开发模型,而且你可能也决定让他们独立开发。本章接下来要讨论的模式的主题就是如何解决这些问题。

14.2 模式:CONTINUOUS INTEGRATION

第14章 保持模型的完整性 - 图3

定义完一个BOUNDED CONTEXT后,必须让它保持合理。

当很多人在同一个BOUNDED CONTEXT中工作时,模型很容易发生分裂。团队越大,问题就越大,但即使是3、4个人的团队也有可能会遇到严重的问题。然而,如果将系统分解为更小的CONTEXT,最终又难以保持集成度和一致性。

有时开发人员没有完全理解其他人所创建的对象或交互的意图,就对它进行了修改,使其失去了原来的作用。有时他们没有意识到他们正在开发的概念已经在模型的另一个部分中实现了,从而导致了这些概念和行为(不正确的)重复。有时他们意识到了这些概念有其他的表示,但却因为担心破坏现有功能而不敢去改动它们,于是他们继续重复开发这些概念和功能。

开发统一的系统(无论规模大小)需要维持很高的沟通水平,而这一点常常很难做到。我们需要运用各种方法来增进沟通并减小复杂性。还需要一些安全防护措施,以避免过于谨慎的行为(例如,开发人员由于担心破坏现有代码而重复开发一些功能)。

极限编程(XP)在这样的环境中真正显示出自己的强大威力。很多XP实践都是针对在很多人频繁更改设计的情况下如何维护设计的一致性这个特定问题而出现的。最纯粹的XP非常适合维护单一BOUNDED CONTEXT中的模型完整性。但是,无论是否使用XP,都很有必要采取CONTINUOUS INTEGRATION过程。

CONTINUOUS INTEGRATION是指把一个上下文中的所有工作足够频繁地合并到一起,并使它们保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。像领域驱动设计中的其他方法一样,CONTINUOUS INTEGRATION也有两个级别的操作:(1) 模型概念的集成;(2) 实现的集成。

团队成员之间通过经常沟通来保证概念的集成。团队必须对不断变化的模型形成一个共同的理解。有很多方法可以帮助做到这一点,但最基本的方法是对UBIQUITOUS LANGUAGE多加锤炼。同时,实际工件通过系统性的合并/构建/测试过程来集成,这样能够尽早暴露出模型的分裂问题。用来集成的过程有很多,大部分有效的过程都具备以下这些特征:

分步集成,采用可重现的合并/构建技术;

自动测试套件;

有一些规则,用来为那些尚未集成的改动设臵一个相当小的生命期上限。

有效过程的另一面是概念集成,虽然它很少被正式地纳入进来。在讨论模型和应用程序时要坚持使用UBIQUITOUS LANGUAGE。

大多数敏捷项目至少每天会把每位开发人员所做的修改合并进来。这个频率可以根据更改的步伐来调整,只要确保该间隔不会导致大量不兼容的工作产生即可。

在MODEL-DRIVEN DESIGN中,概念集成为实现集成铺平了道路,而实现集成验证了模型的有效性和一致性,并暴露出模型的分裂问题。

因此:

建立一个把所有代码和其他实现工件频繁地合并到一起的过程,并通过自动化测试来快速查明模型的分裂问题。严格坚持使用UBIQUITOUS LANGUAGE,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识。

最后,不要在持续集成中做一些不必要的工作。CONTINUOUS INTEGRATION只有在BOUNDED CONTEXT中才是重要的。相邻CONTEXT中的设计问题(包括转换)不必以同一个步调来处理。

CONTINUOUS INTEGRATION可以在任何单独的BOUNDED CONTEXT中使用,只要它的工作规模大到需要两个以上的人去完成就可以。它可以维护单一模型的完整性。当多个BOUNDED CONTEXT共存时,我们必须要确定它们的关系,并设计任何必需的接口。

14.3 模式:CONTEXT MAP

第14章 保持模型的完整性 - 图4

只有一个BOUNDED CONTEXT并不能提供全局视图。其他模型的上下文可能仍不清楚而且还在不断变化。

其他团队中的人员并不是十分清楚CONTEXT的边界,他们会不知不觉地做出一些更改,从而使边界变得模糊或者使互连变得复杂。当不同的上下文必须互相连接时,它们可能会互相重叠。

BOUNDED CONTEXT之间的代码重用是很危险的,应该避免。功能和数据的集成必须要通过转换去实现。通过定义不同上下文之间的关系,并在项目中创建一个所有模型上下文的全局视图,可以减少混乱。

CONTEXT MAP位于项目管理和软件设计的重叠部分。按照常规,人们往往按团队组织的轮廓来划定边界。紧密协作的人会很自然地共享一个模型上下文。不同团队的人员(或者在同一个团队中但从不交流的人)将使用不同的上下文。办公室的物理位臵也有影响,例如,分别位于大楼两端的团队成员(更不用说在不同城市工作的人了)如果没有为整合做额外的工作,很有可能会使用不同的上下文。大多数项目经理会本能地意识到这些因素,并围绕子系统大致把各个团队组织起来。但团队组织与软件模型及设计之间的相互关系仍然不够明显。对于软件模型与设计的持续概念细分,项目经理和团队成员需要一个清晰的视图。

因此:

识别在项目中起作用的每个模型,并定义其BOUNDED CONTEXT。这包括非面向对象子系统的隐含模型。为每个BOUNDED CONTEXT命名,并把名称添加到UBIQUITOUS LANGUAGE中。

描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。

先将当前的情况描绘出来。以后再做改变。

在每个BOUNDED CONTEXT中,都将有一种一致的UBIQUITOUS LANGUAGE的“方言”。我们需要把BOUNDED CONTEXT的名称添加到该方言中,这样只要通过明确CONTEXT就可以清楚地讨论任意设计部分的模型。

CONTEXT MAP无需拘泥于任何特定的文档格式。我发现类似本章的简图在可视化和沟通上下文图方面很有帮助。有些人可能喜欢使用较多的文本描述或别的图形表示。在某些情况下,团队成员之间的讨论就足够了。需求不同,细节层次也不同。不管CONTEXT MAP采用什么形式,它必须在所有项目人员之间共享,并被他们理解。它必须为每个BOUNDED CONTEXT提供一个明确的名称,而且必须阐明联系点和它们的本质。

根据设计问题和项目组织问题的不同,BOUNDED CONTEXT之间的关系有很多种形式。本章稍后将介绍CONTEXT之间的各种关系模式,这些模式分别适用于不同的情况,并且提供了一些术语,这些术语可以用来描述你在自己的上下文图中发现的关系。记住,CONTEXT MAP始终表示它当前所处的情况,你所发现的关系一开始可能并不适合这些模式。如果它们与某种模式非常接近,你可能想用这个模式名来描述它们,但不要生搬硬套。只需描述你所发现的关系即可。过后,你可以向更加标准化的关系过渡。

那么,如果你发现模型产生了分裂——模型完全混乱且包含不一致时,你该怎么办呢?这时一定要十分注意,先把描述工作停下来。然后,从精确的全局角度来解决这些混乱点。小的分裂可以修复,并且可以通过实施一些过程来为修复提供支持。如果关系很模糊,可以选择一种最接近的模式,然后向此模式靠拢。最重要的任务是画出一个清晰的CONTEXT MAP,而这可能意味着修复实际发现的问题。但不要因为修复必要的问题而重组整个结构。我们只需修改那些明显的矛盾即可,直到得出一个明确的CONTEXT MAP,在这个图中,你的所有工作都被放到某个BOUNDED CONTEXT中,而且所有互连的模型都有明确的关系。

一旦获得了一致的CONTEXT MAP,就会看到需要修改的那些地方。在经过深思熟虑后,你可以调整团队的组织或设计。记住,在更改实际上完成以前,不要先修改CONTEXT MAP。

示例 运输应用程序中的两个CONTEXT

我们再次回到运输系统。应用程序的主要特性之一是在客户预订的时候自动为货物安排路线。模型类似于图14-2。

Routing Service是一个SERVICE,它把服务的机制封装在一个INTENTION-REVEALING INTERFACE后面,这个接口是由一些SIDE-EFFECT-FREE FUNCTION构成的。这些函数的结果是用ASSERTION刻画的。

(1) 接口声明了当传入一个Route Specification时,将返回一个Itinerary。

(2) ASSERTION规定返回的Itinerary将满足所传入的Route Specification。

从上面这些并不能看出这项困难任务是如何执行的。现在,让我们来看一下幕后的机制。

第14章 保持模型的完整性 - 图5 图14-2

最初在这个示例所在的项目中,我在Routing Service的内部机制上太过教条了。我希望把领域模型扩展一下,以便把实际的路线安排操作包括进来,由模型来表示航名航次,并直接把这些航名航次与Itinerary中的Leg(航段)关联起来。但负责处理路线问题的团队指出,为了更好地执行路线安排,并充分利用那些成熟的算法,应该把这个解决方案实现为一个优化网络,并把航次的每个航段表示为矩阵中的一个元素。他们坚持要用一个完全不同的运输作业模型来实现此目的。

就当时的设计而言,他们在路线安排过程的计算要求上无疑是正确的,而且我也没有更好的思路,因此我只好同意了。实际上,我们创建了两个独立的BOUNDED CONTEXT,每个上下文都有各自运输作业的概念组织(参见图14-3)。

我们需要接受一个Routing Service请求,并将它转换为Network Traversal Service可以理解的术语,然后获取结果,并将其转换为Routing Service所期望得到的格式。

这意味着并不需要映射这两个模型中的所有事物,而只要能够进行这两个特定的转换即可:

Route Specification→地点代码的列表

Node标识的列表→Itinerary

为了进行这两个转换,我们必须研究元素在一个模型中的含义,并弄清楚如何在另一个模型中把它表示出来。

我们从第一个转换开始(Route Specification→地点代码的列表),我们必须考虑列表中的地点序列的含义。列表中的第一项是路线的开始,然后必须依次通过每个地点,直到到达列表中的最后一个地点。因此,起点和目的地分别是列表中的第一项和最后一项,中间(如果有的话)则是清关地点(参见图4-14)。

第14章 保持模型的完整性 - 图6 图14-3 同时使用两个BOUNDED CONTEXT,这样就可以应用有效的路线安排算法

第14章 保持模型的完整性 - 图7 图14-4 对Network Traversal Service的一次查询的转换

(幸运的是,两个团队使用相同的地点代码,因此我们不必处理地点代码之间的转换。)

注意,反向转换是不明确的,因为网络遍历的输入允许任意数目的中间点,而不是只有特别指定的清关点。幸运的是,由于我们并不需要反向转换,因此不会产生这个问题,但由此我们也了解到为什么有些转换是不可能的。

现在,我们开始对结果进行转换(Node标识的列表→Itinerary)。假设我们可以根据所得到的Node ID来使用Repository查询Node和Shipping Operation对象。那么,这些Node是如何映射到Leg上的呢?根据operationType-Code,我们可以把Node列表分解为“出发/到达”对。每一对组成一个Leg。

第14章 保持模型的完整性 - 图8 图14-5 对Network Traversal Service所发现的一个路线进行转换

每个Node对的属性按下面这样进行映射:

➝➝➝➝➝

这是两个模型之间的概念转换映射。现在,我们必须通过某种方法来实现这些转换。在像这样的简单例子中,我通常先创建一个用于转换的对象,然后找到或创建另一个对象来为子系统的其余部分提供服务。

第14章 保持模型的完整性 - 图10 图14-6 双向转换器

这是两个团队必须一起维护的对象。设计应该使其易于单元测试,因此最好让两个团队协作开发一个测试套件。除此之外,他们可以采用不同的方式各自开发。

第14章 保持模型的完整性 - 图11 图14-7

Routing Service的实现现在变成了把任务委托给Translator和Network Traversal Service。其唯一的操作可能如下面代码所示:

第14章 保持模型的完整性 - 图12

这种处理方法还不错。BOUNDED CONTEXT使每个模型都保持相对整洁,使团队很大程度上彼此独立工作,而且,如果最初的假设是正确的,它们可能会发挥很好的作用(本章后面还会回头讨论这个问题)。

两个上下文之间的接口非常小。Routing Service的接口把预订上下文中的其余部分与路线查找事件隔离开。这个接口完全由SIDE-EFFECT-FREE FUNCTION构成,因此很容易测试。与其他CONTEXT和谐共存的一个秘诀是拥有有效的接口测试集。正如里根总统在裁减核武器谈判时所说的名言“信任,但要确认”[1]

我们很容易设计一组自动测试集来把Route Specification输入到Routing Service中并检查返回的Itinerary。

模型上下文总是存在的,但如果我们不注意的话,它们可能会发生重叠和变化。通过明确地定义BOUNDED CONTEXT和CONTEXT MAP,团队就可以掌控模型的统一过程,并把不同的模型连接起来。

14.3.1 测试CONTEXT的边界

对各个BOUNDED CONTEXT的联系点的测试特别重要。这些测试有助于解决转换时所存在的一些细微问题以及弥补边界沟通上存在的不足。测试充当了有用的早期报警系统,特别是在我们必须信赖那些模型细节却又无法控制它们时,它能让我们感到放心。

14.3.2 CONTEXT MAP的组织和文档化

这里只有以下两个重点。

(1) BOUNDED CONTEXT应该有名称,以便可以讨论它们。这些名称应该被添加到团队的UBIQUITOUS LANGUAGE中。

(2) 每个人都应该知道边界在哪里,而且应该能够分辨出任何代码段的CONTEXT,或任何情况的CONTEXT。

有很多种方式可以满足第二项需求,这取决于团队的文化。一旦定义了BOUNDED CONTEXT,那么把不同上下文的代码隔离到不同的MODULE中就再自然不过了,但这样就产生了一个问题——如何跟踪哪个MODULE属于哪个CONTEXT。我们可以用命名规范来表明这一点,或者使用其他简单且不会产生混淆的机制。

同样重要的是以一种适当的形式来传达概念边界,以使团队中的每个人都能以相同的方式来理解它们。就沟通而言,我喜欢用非正式的图,就像示例中所显示的那些图一样。也可以使用更严格的图或文本列表来显示每个CONTEXT中的所有包,同时显示出联系点以及负责连接和转换的机制。有些团队更愿意使用这种方法,而另一些团队通过口头协定和大量的讨论也能很好地实现这一目的。

无论是哪种情况,将CONTEXT MAP融入到讨论中都是至关重要的,前提是CONTEXT的名称要添加到UBIQUITOUS LANGUAGE中。不要说“George团队的内容改变了,因此我们也需要改变那些与其进行交互的内容”,而应该说:“Transport Network模型发生了改变,因此我们也需要修改Booking上下文的转换器。”

14.4 BOUNDED CONTEXT之间的关系

下面介绍的这些模式涵盖了将两个模型关联起来的众多策略。把模型连接到一起之后,就能够把整个企业笼括在内。这些模式有着双重目的,一是为成功地组织开发工作设定目标,二是为描述现有组织提供术语。

现有关系可能与这些模式中的某一种很接近——这可能是由于巧合,也可能是有意设计的——在这种情况下可以使用这个模式的术语来描述关系,但差异之处应该引起重视。然后,随着每次小的设计修改,关系会与所选定的模式越来越接近。

另一方面,你可能会发现现有关系很混乱或过于复杂。要想得到一个明确的CONTEXT MAP,需要重新组织一些关系。在这种情况或任何需要考虑重组的情况下,这些模式提供了应对各种不同情况的选择。这些模式的主要区别包括你对另一个模型的控制程度、两个团队之间合作水平和合作类型,以及特性和数据的集成程度。

下面这些模式涵盖了一些最常见和最重要的情况,它们提供了一些很好的思路,沿着这些思路,我们就可以知道如何处理其他情况。开发一个紧密集成产品的优秀团队可以部署一个大的、统一的模型。如果团队需要为不同的用户群提供服务,或者团队的协调能力有限,可能就需要采用SHARED KERNEL(共享内核)或CUSTOMER/SUPPLIER(客户/供应商)关系。有时仔细研究需求之后可能发现集成并不重要,而系统最好采用SEPARATE WAY(各行其道)模式。当然,大多数项目都需要与遗留系统或外部系统进行一定程度的集成,这就需要使用OPEN HOST SERVICE(开放主机服务)或ANTICORRUPTION LAYER(防护层)。

14.5 模式:SHARED KERNEL

第14章 保持模型的完整性 - 图13

当功能集成受到局限,CONTINUOUS INTEGRATION的开销可能会变得非常高。尤其是当团队的技能水平或行政组织不能保持持续集成,或者只有一个庞大的、笨拙的团队时,更容易发生这种情况。在这种情况下就要定义单独的BOUNDED CONTEXT,并组织多个团队。

当不同团队开发一些紧密相关的应用程序时,如果团队之间不进行协调,即使短时间内能够取得快速进展,但他们开发出的产品可能无法结合到一起。最后可能不得不耗费大量精力在转换层上,并且频繁地进行改动,不如一开始就使用CONTINUOUS INTEGRATION那么省心省力,同时这也造成重复工作,并且无法实现公共的UBIQUITOUS LANGUAGE所带来的好处。

在很多项目中,我看到一些基本上独立工作的团队共享基础设施层。领域工作采用类似的方法也可以得到很好的效果。保持整个模型和代码完全同步的开销可能太高了,但从系统中仔细挑选出一部分并保持同步,就能以较小的代价获得较大的收益。

因此:

从领域模型中选出两个团队都同意共享的一个子集。当然,除了这个模型子集以外,还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位,一个团队在没与另一个团队商量的情况下不应擅自更改它。

功能系统要经常进行集成,但集成的频率应该比团队中CONTINUOUS INTEGRATION的频率低一些。在进行这些集成的时候,两个团队都要运行测试。

这是一个仔细的平衡。SHARED KERNEL(共享内核)不能像其他设计部分那样自由更改。在做决定时需要与另一个团队协商。共享内核中必须集成自动测试套件,因为修改共享内核时,必须要通过两个团队的所有测试。通常,团队先修改各自的共享内核副本,然后每隔一段时间与另一个团队的修改进行集成。例如,在每天(或更短的时间周期)进行CONTINUOUS INTEGRATION的团队中,可以每周进行一次内核的合并。不管代码集成是怎样安排的,两个团队越早讨论修改,效果就会越好。

SHARED KERNEL通常是CORE DOMAIN,或是一组GENERIC SUBDOMAIN(通用子领域),也可能二者兼有(参见第15章),它可以是两个团队都需要的任何一部分模型。使用SHARED KERNEL的目的是减少重复(并不是消除重复,因为只有在一个BOUNDED CONTEXT中才能消除重复),并使两个子系统之间的集成变得相对容易一些。

14.6 模式:CUSTOMER/SUPPLIER DEVELOPMENT TEAM

第14章 保持模型的完整性 - 图14

我们常常会碰到这样的情况:一个子系统主要服务于另一个子系统;“下游”组件执行分析或其他功能,这些功能向“上游”组件反馈的信息非常少,所有依赖都是单向的。两个子系统通常服务于完全不同的用户群,其执行的任务也不同,在这种情况下使用不同的模型会很有帮助。工具集可能也不相同,因此无法共享程序代码。

上游和下游子系统很自然地分隔到两个BOUNDED CONTEXT中。如果两个组件需要不同的技能或者不同的工具集来实现时,更需要把它们隔离到不同的上下文中。转换很容易,因为只需要进行单向转换。但两个团队的行政组织关系可能会引起问题。

如果下游团队对变更具有否决权,或请求变更的程序太复杂,那么上游团队的开发自由度就会受到限制。由于担心破坏下游系统,上游团队甚至会受到抑制。同时,由于上游团队掌握优先权,下游团队有时也会无能为力。

下游团队依赖于上游团队,但上游团队却不负责下游团队的产品交付。要琢磨拿什么来影响对方团队,是人性呢,还是时间压力,亦或其他诸如此类的,这需要耗费大量额外的精力。因此,正式规定团队之间的关系会使所有人工作起来更容易。这样,就可以对开发过程进行组织,均衡地处理两个用户群的需求,并根据下游所需的特性来安排工作。

在极限编程项目中,已经有了实现此目的的机制——迭代计划过程。我们只需根据计划过程来定义两个团队之间的关系。下游团队的代表类似于用户代表,参加上游团队的计划会议,上游团队直接与他们的“客户”同仁讨论和权衡其所需的任务。结果是供应商团队得到一个包含下游团队最需要的任务的迭代计划,或是通过双方商定推迟一些任务,这样下游团队也就知道这些被推迟的功能不会交付给他们。

如果使用的不是XP过程,那么无论使用什么类似的方法来平衡不同用户的关注点,都可以对这种方法加以扩充,使之把下游应用程序的需求包括进来。

因此:

在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。

两个团队共同开发自动化验收测试,用来验证预期的接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对下游团队产生副作用。

在迭代期间,下游团队成员应该像传统的客户一样随时回答上游团队的提问,并帮助解决问题。

自动化验收测试是这种客户关系的一个重要部分。即使在合作得非常好的项目中,虽然客户很明确他们所依赖的功能并告诉上游团队,而且供应商也能很认真地把所做的修改传递给下游团队,但如果没有测试,仍然会发生一些很意外的事情。这些事情将破坏下游团队的工作,并使上游团队不得不采取计划外的紧急修复措施。因此,客户团队在与供应商团队合作的过程中,应该开发自动验收测试来验证所期望的接口。上游团队将把这些测试作为标准测试套件的一部分来运行。任何一个团队在修改这些测试时都需要与另一个团队沟通,因为修改测试就意味着修改接口。

当某个客户对供应商的业务至关重要时,不同公司的项目之间也会出现客户/供应商关系。下游团队也能制约上游团队,一个有影响力的客户所提出的要求对上游项目的成功非常重要,但这些要求也能破坏上游项目的开发。建立正式的需求响应过程对双方都有利,因为与内部IT关系相比,在这种外部关系中更难做出“成本/效益”的权衡。

这种模式有两个关键要素。

(1) 关系必须是客户与供应商的关系,其中客户的需求是至关重要的。由于下游团队并不是唯一的客户,因此不同客户的要求必须通过协商来平衡,但这些要求都是非常重要的。这种关系与那种经常出现的“穷亲威”关系相反,在后者的关系中,下游团队不得不乞求上游团队满足其需求。

(2) 必须有自动测试套件,使上游团队在修改代码时不必担心破坏下游团队的工作,并使下游团队能够专注于自己的工作,而不用总是密切关注上游团队的行动。

在接力赛中,前面的选手在接棒的时候不能一直回头看,这位选手必须相信队友能够把接力棒准确地交到他手中,否则整个团队的速度无疑会慢下来。

示例 收益分析与预订

我们再次回到运输示例中。公司组建了一支专门的团队,负责分析公司收到的所有预订,看看如何实现收益的最大化。团队成员可能发现货轮上还有空位臵,并建议接受更多超订。他们可能发现货轮过早地装满了散装货物,从而使公司不得不拒绝利润更大的特殊货物。在这种情况下,他们可能会建议为这类货物预留空间,或是提高散货的运输价格。

为了进行这种分析,他们使用自己的复杂模型。在实现过程中,他们使用了一个带有构建分析模型工具的数据仓库。而且他们需要从预订应用程序中获取大量信息。

从一开始就知道,这显然是两个BOUNDED CONTEXT,因为它们使用不同的实现工具,而且最重要的是,它们使用不同的领域模型。那么它们之间应该具有什么样的关系呢?

在这种情况下使用SHARED KERNEL看起来很合乎逻辑,因为收益分析只对预订模型的一个子集感兴趣,而且它们自己的模型也有一些诸如货物、价格等的重叠概念。但是,当使用了不同的实现技术时,SHARED KERNEL是很难做到的。此外,收益分析团队需要建立非常专门的模型,他们要不断修改模型,并且尝试其他的模型。他们最好从预订CONTEXT中找到所需的东西,并把它们转换到自己的上下文中。(另一方面,如果他们使用SHARED KERNEL,他们的翻译负担将会轻得多。他们仍然必须重新实现模型,并把数据转换到新的实现中,但如果模型相同的话,转换就简单多了。)

预订应用程序并不依赖收益分析,因为并没有打算做自动调整策略。调整决策将由专家来制定,并传递给相关的人员和系统。这样我们就有了一个上游/下游关系。下游的需求如下:

(1) 一些数据(任何预订操作都不需要这些数据);

(2) 数据库模式具有一定稳定性(或至少具有可靠的变更通知机制),或者一个用于导出的实用程序。

幸运的是,预订应用程序开发团队的项目经理非常积极主动地帮助收益分析团队。原本以为两个团队的合作会是个问题,因为实际负责处理日常预订业务的运营部门和实际执行收益分析的团队并非向同一个副总裁报告工作。但高管层非常关心收益管理,而且过去曾看到过两个部门之间的合作问题,因此调整了一下软件开发项目的结构,让两个团队的项目经理向同一个人汇报工作。

这样,应用CUSTOMER/SUPPLIER DEVELOPMENT TEAM(客户/供应商开发团队)的所有要求都满足了。

我曾经看到过这种场景出现在很多地方,其中分析软件开发人员和操作软件开发人员具有客户/供应商关系。当上游团队成员认为他们的角色是服务于客户时,工作会进展得相当顺利。这种关系几乎总是非正式地组织起来的,因此工作顺利与否有赖于两个项目经理的私人关系。

在一个XP项目中,我曾经看到过正式的客户/供应商关系,在每次迭代中,下游团队的代表以客户的身份参与到计划过程中,他们与更为传统的(应用程序功能的)客户代表聚到一起,共同协商哪些任务应该被添加到迭代计划中。这是一家小公司的项目,因此最近一级的共同主管不会处在关系链的很远位臵。项目进展得非常顺利。

CUSTOMER/SUPPLIER TEAM涉及的团队如果能在同一个部门中工作,最后会形成共同的目标,这样成功机会将更大一些,如果两个团队分属不同的公司,但实际上也具有这些角色,同样也容易成功。但是,当上游团队不愿意为下游团队提供服务时,情况就会完全不同……

14.7 模式:CONFORMIST

第14章 保持模型的完整性 - 图15

当两个具有上游/下游关系的团队不归同一个管理者指挥时,CUSTOMER/SUPPLIER TEAM这样的合作模式就不会奏效。勉强应用这种模式会给下游团队带来麻烦。大公司可能会发生这种情况,其中两个团队在管理层次中相隔很远,或者两个团队的共同主管不关心它们之间的关系。当两个团队属于不同公司时,如果客户的业务对供应商不是非常重要,那么也会出现这种情况。或许供应商有很多小客户,或者供应商正在改变市场方向,而不再重视老客户。也可能是供应商的运营状况较差,或者已经倒闭。不管是什么原因,现实情况是下游团队只能靠自己了。

当两个开发团队具有上/下游关系时,如果上游团队没有动力来满足下游团队的需求,那么下游团队将无能为力。出于利他主义的考虑,上游开发人员可能会做出承诺,但他们可能不会履行承诺。下游团队出于良好的意愿会相信这些承诺,从而根据一些永远不会实现的特性来制定计划。下游项目只能被搁臵,直到团队最终学会利用现有条件自力更生为止。下游团队不会得到根据他们的需求而量身定做的接口。

在这种情况下,有3种可能的解决途径。一种是完全放弃对上游的使用。做出这种选择时,应进行切实地评估,绝不要假定上游会满足下游的需求。有时我们会高估这种依赖性的价值,或是低估它的成本。如果下游团队决定切断这条链,他们将走上SEPARATE WAY(各行其道)的道路(参见本章后面介绍的模式)。

有时,使用上游软件具有非常大的价值,因此必须保持这种依赖性(或者是行政决策规定团队不能改变这种依赖性)。在这种情况下,还有两种途径可供选择,选择哪一种取决于上游设计的质量和风格。如果上游的设计很难使用(可能是由于缺乏封装、使用了不恰当的抽象或者建模时使用了下游团队无法使用的范式),那么下游团队仍然需要开发自己的模型。他们将担负起开发转换层的全部责任,这个层可能会非常复杂(参见本章后面要介绍的ANTICORRUPTION LAYER)。

跟随并不总是坏事

当使用一个具有很大接口的现成组件时,一般应该遵循(CONFORM)该组件中隐含的模型。组件和你自己的应用程序显然是不同的BOUNDED CONTEXT,因此根据团队组织和控制的不同,可能需要使用适配器来进行一点点格式转换,但模型一定要保持相同。否则,就应该质疑使用该组件的价值。如果它确实能够提供价值,那说明它的设计中已经消化吸收了一些知识。在该组件的应用范围内,它可能比你的理解要深入。你的模型大概会超出该组件的范围,而且这些超出部分将演化出你自己的概念。但在两者连接的地方,你的模型将是一个CONFORMIT,遵从组件模型的领导。实际上,你将被带到一个更好的设计中。

当你与组件的接口很小时,那么共享一个统一模型就不那么重要了,而且转换也是个可行的选项。但是,当接口很大而且集成更加重要时,跟随通常是有意义的。

另一方面,如果上游设计的质量不是很差,而且风格也能兼容的话,那么最好不要再开发一个独立的模型。这种情况下可以使用CONFORMIST(跟随者)模式。

因此:

通过严格遵从上游团队的模型,可以消除在BOUNDED CONTEXT之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择CONFORMITY模式可以极大地简化集成。此外,这样还可以与供应商团队共享UBIQUITOUS LANGUAGE。供应商处于统治地位,因此最好使沟通变容易。他们从利他主义的角度出发,会与你分享信息。

这个决策会加深你对上游团队的依赖,同时你的应用也受限于上游模型的功能,充其量也只能做一些简单的增强而已。人们在主观上不愿意这样做,因此有时本应该这样选择时,却没有这样选择。

如果这些折中不可接受,而上游的依赖又必不可少,那么还可以选择第二种方法。通过创建一个ANTICORRUPTION LAYER来尽可能把自己隔离开,这是一种实现转换映射的积极方法,后面将会讨论它。

CONFORMIST模式类似于SHARED KERNEL模式。在这两种模式中,都有一个重叠的区域——在这个重叠区域内模型是相同的,此外还有你的模型所扩展的部分,以及另一个模型对你没有影响的部分。这两种模式之间的区别在于决策制定和开发过程不同。SHARED KERNEL是两个高度协调的团队之间的合作模式,而CONFORMIST模式则是应对与一个对合作不感兴趣的团队进行集成。

前面介绍了在两个BOUNDED CONTEXT之间集成时可以进行的各种合作,从高度合作的SHARED KERNEL模式或CUSTOMER/SUPPLIER DEVELOPER TEAM到单方面的CONFORMIST模式。现在,我们最后来看一种更悲观的关系,假设另一个团队既不合作,而且其设计也无法使用时,该如何应对。

14.8 模式:ANTICORRUPTION LAYER

第14章 保持模型的完整性 - 图16

新系统几乎总是需要与遗留系统或其他系统进行集成,这些系统具有其自己的模型。当把参与集成的BOUNDED CONTEXT设计完善并且团队相互合作时,转换层可能很简单,甚至很优雅。但是,当边界那侧发生渗透时,转换层就要承担起更多的防护职责。

当正在构建的新系统与另一个系统的接口很大时,为了克服连接两个模型而带来的困难,新模型所表达的意图可能会被完全改变,最终导致它被修改得像是另一个系统的模型了(以一种特定的风格)。遗留系统的模型通常很弱。即使对于那些模型开发得很好的例外情况,它们可能也不符合当前项目的需要。然而,集成遗留系统仍然具有很大的价值,而且有时还是绝对必要的。

正确答案是不要全盘封杀与其他系统的集成。在我经历过的一些项目中,人们非常热衷于替换所有遗留系统,但由于工作量太大,这不可能立即完成。此外,与现有系统集成是一种有价值的重用形式。在大型项目中,一个子系统通常必须与其他独立开发的子系统连接。这些子系统将从不同角度反映问题领域。当基于不同模型的系统被组合到一起时,为了使新系统符合另一个系统的语义,新系统自己的模型可能会被破坏。即使另一个系统被设计得很好,它也不会与客户基于同一个模型。而且其他系统往往并不是设计得很好。

当通过接口与外部系统连接时,存在很多障碍。例如,基础设施层必须提供与另一个系统进行通信的方法,那个系统可能处于不同的平台上,或是使用了不同的协议。你必须把那个系统的数据类型转换为你自己系统的数据类型。但通常被忽视的一个事实是那个系统肯定不会使用相同的概念领域模型。

如果从一个系统中取出一些数据,然后在另一个系统中错误地解释了它,那么显然会发生错误,甚至会破坏数据库。尽管我们已经认识到这一点,这个问题仍然会“偷袭”我们,因为我们认为在系统之间转移的是原始数据,其含义是明确的,并且认为这些数据在两个系统中的含义肯定是相同的。这种假设常常是错误的。数据与每个系统的关联方式会使数据的含义出现细微但重要的差别。而且,即使原始数据元素确实具有完全相同的含义,但在原始数据这样低的层次上进行接口操作通常是错误的。这样的底层接口使另一个系统的模型丧失了解释数据以及约束其值和关系的能力,同时使新系统背负了解释原始数据的负担(而且并未使用这些数据自己的模型)。

我们需要在不同模型的关联部分之间建立转换机制,这样模型就不会被未经消化的外来模型元素所破坏。

因此:

创建一个隔离层,以便根据客户自己的领域模型来为客户提供相关功能。这个层通过另一个系统现有接口与其进行对话,而只需对那个系统作出很少的修改,甚至无需修改。在内部,这个层在两个模型之间进行必要的双向转换。

这种连接两个系统的机制可能会使我们想到把数据从一个程序传输到另一个程序,或者从一个服务器传输到另一个服务器。我们很快就会讨论技术通信机制的使用。但这些细节问题不应与ANTICORRUPTION LAYER混淆,因为ANTICORRUPTION LAYER并不是向另一个系统发送消息的机制。相反,它是在不同的模型和协议之间转换概念对象和操作的机制。

ANTICORRUPTION LAYER本身就可能是一个复杂的软件。接下来将概要描述在创建ANTICORRUPTION LAYER时需要考虑的一些事项。

14.8.1 设计ANTICORRUPTION LAYER的接口

ANTICORRUPTION LAYER的公共接口通常以一组SERVICE的形式出现,但偶尔也会采用ENTITY的形式。构建一个全新的层来负责两个系统之间的语义转换为我们提供了一个机会,它使我们能够重新对另一个系统的行为进行抽象,并按照与我们的模型一致的方式把服务和信息提供给我们的系统。在我们的模型中,把外部系统表示为一个单独的组件可能是没有意义的。最好是使用多个SERVICE(或偶尔使用ENTITY),其中每个SERVICE都使用我们的模型来履行一致的职责。

14.8.2 实现ANTICORRUPTION LAYER

对ANTICORRUPTION LAYER设计进行组织的一种方法是把它实现为FACADE、ADAPTER(这两种模式来自[Gamma et al.1995])和转换器的组合,外加两个系统之间进行对话所需的通信和传输机制。

我们常常需要与那些具有大而复杂、混乱的接口的系统进行集成。这不是概念模型差别的问题(概念模型差别是我们使用ANTICORRUPTION LAYER的动机),而是一个实现问题。当我们尝试创建ANTICORRUPTION LAYER时,会遇到这个实现问题。当从一个模型转换到另一个模型的时候(特别是当一个模型很混乱时),如果不能同时处理那些难于沟通的子系统接口,那么将很难完成。好在FACADE可以解决这个问题。

FACADE是子系统的一个可供替换的接口,它简化了客户访问,并使子系统更易于使用。由于我们非常清楚要使用另一个系统的哪些功能,因此可以创建FACADE来促进和简化对这些特性的访问,并把其他特性隐藏起来。FACADE并不改变底层系统的模型。它应该严格按照另一个系统的模型来编写。否则会产生严重的后果:轻则导致转换职责蔓延到多个对象中,并加重FACADE的负担;重则创建出另一个模型,这个模型既不属于另一个系统,也不属于你自己的BOUNDED CONTEXT。FACADE应该属于另一个系统的BOUNDED CONTEXT,它只是为了满足你的专门需要而呈现出的一个更友好的外观。

ADAPTER是一个包装器,它允许客户使用另外一种协议,这种协议可以是行为实现者不理解的协议。当客户向适配器发送一条消息时,ADAPTER把消息转换为一条在语义上等同的消息,并将其发送给“被适配者”(adaptee)。之后ADAPTER对响应消息进行转换,并将其发回。我在这里使用适配器(adapter)这个术语略微有点儿不严谨,因为[Gamma et al.1995]一书中强调的是使包装后的对象符合客户所期望的标准接口,而我们选择的是被适配的接口,而且被适配者甚至可能不是一个对象。我们强调的是两个模型之间的转换,但我认为这与ADAPTER的意图是一致的。

我们所定义的每种SERVICE都需要一个支持其接口的ADAPTER,这个适配器还需要知道怎样才能向其他系统及其FACADE发出相应的请求)。

剩下的要素就是转换器了。ADAPTER的工作是知道如何生成请求。概念对象或数据的实际转换是一种完全不同的复杂任务,我们可以让一个单独的对象来承担这项任务,这样可以使负责转换的对象和ADAPTER都更易于理解。转换器可以是一个轻量级的对象,它可以在需要的时候被实例化。由于它只属于它所服务的ADAPTER,因此不需要有状态,也不需要是分布式的。

这些都是我用来创建ANTICORRUPTION LAYER的基本元素。此外,还有其他一些需要考虑的因素。

如图14-8所示,一般是由正在设计的系统(你的子系统)来发起一个动作。但在有些情况下,其他子系统可能需要向你的子系统提交某种请求,或是把某个事件通知给你的子系统。ANTICORRUPTION LAYER可以是双向的,它可能使用具有对称转换的相同转换器来定义两个接口上的SERVICE(并使用各自的ADAPTER)。尽管实现ANTICORRUPTION LAYER通常不需要对另一个子系统做任何修改,但为了使它能够调用ANTICORRUPTION LAYER的SERVICE,有时还是有必要修改的。

第14章 保持模型的完整性 - 图17 图14-8 ANTICORRUPTION LAYER的结构

我们通常需要一些通信机制来连接两个子系统,而且它们可能位于不同的服务器上。在这种情况下,必须决定在哪里放臵通信链接。如果无法访问另一个子系统,那么可能必须在FACADE和另一个子系统之间设臵通信链接。但是,如果FACADE可以直接与另一个子系统集成到一起,那么在适配器和FACADE之间设臵通信链接也不失为一种好的选择,这是因为FACADE的协议比它所封装的内容要简单。在有些情况下,整个ANTICORRUPTION LAYER可以与另一个子系统放在一起,这时可以在你的系统和构成ANTICORRUPTION LAYER接口的SERVICE之间设臵通信链接或分发机制。这些都是需要根据实际情况做出的实现和部署决策。它们与ANTICORRUPTION LAYER的概念角色无关。

如果有权访问另一个子系统,你可能会发现对它进行少许的重构会使你的工作变得更容易。特别是应该为那些需要使用的功能编写更显式的接口,如果可能的话,首先从编写自动测试开始。

当需要进行广泛的集成时,转换的成本会直线上升。这时需要对正在设计的系统的模型做出一些选择,使之尽量接近外部系统,以便使转换更加容易。做这些工作时要非常小心,不要破坏模型的完整性。这是只有当转换的难度无法掌控时才选择进行的事情。如果这种方法看起来是大部分重要问题的最自然的解决方案,那么可以考虑让你的子系统采用CONFORMIST模式,从而消除转换。

如果另一个子系统很简单或有一个很整洁的接口,可能就不需要FACADE了。

如果一个功能是两个系统的关系所需的,就可以把这个功能添加到ANTICORRUPTION LAYER中。此外我们还很容易想到两个特性,一是外部系统使用情况的审计跟踪,二是追踪逻辑,其用于调试对另一个接口的调用。

记住,ANTICORRUPTION LAYER是连接两个BOUNDED CONTEXT的一种方式。我们常常需要使用别人创建的系统,然而我们并未完全理解这些系统,并且也无法控制它们。但这并不是我们需要在两个子系统之间使用防护层的唯一情况。如果你自己开发的两个子系统基于不同的模型,那么使用ANTICORRUPTION LAYER把它们连接起来也是有意义的。在这种情况下,你应该可以完全控制这两个子系统,而且通常可以使用一个简单的转换层。但是,如果这两个BOUNDED CONTEXT采用了SEPARATE WAY模式,而仍然需要进行一定的功能集成,那么可以使用ANTICORRUPTION LAYER来减少它们之间的矛盾。

示例 遗留预订应用程序

为了有一个小的、可以快速开始的最初版本,我们将编写一个最小化的应用程序,它可以建立一次装运(shipment)并通过一个转换层传递给遗留系统进行预订和支持操作。由于我们是专门为了保护正在开发的模型不受遗留设计的影响才构建的转换层,因此这个转换就是一个ANTICORRUPTION LAYER。

最初,ANTICORRUPTION LAYER将接收表示装载的对象,对它们进行转换并传递给遗留系统,请求一个预订,然后捕获确认消息并将其转换成新设计的确认对象。这种隔离使我们基本上能够独立于遗留系统来开发新的应用程序,尽管这也必须投入相当多的转换工作。

在后续的每个版本中,根据后面的决策,新系统要么可以接管遗留系统的更多功能,要么可以在不替换现有功能的情况下增加一些新的功能。这种灵活性,以及能够持续地操作合并的系统并同时进行新老系统的逐步过渡,会使我们在构建ANTICORRUPTION LAYER上投入的工作变得有价值。

14.8.3 一个关于防御的故事

为了保护边境不受周边好战的游牧部落的侵犯,古代中国修建了长城。虽然它并不是一道不可逾越的屏障,但它却使得与邻近地区的通商变得规范有序,同时也可以抵御侵略和其他不良影响。两千多年来,它定义了一个边界,保护中国的农业文明较少受到外界混乱局面的干扰。

如果没有长城,中国可能不会形成如此独特的文明,但尽管如此,长城的修建耗资巨大,它至少使一个朝代“破产”,而且也可能导致了它最终灭亡。隔离策略的益处必须平衡它产生的代价。我们应该从实际出发,对模型做出适度的修改,使之能够更好地适应外部模型。

任何集成都是有开销的,无论这种集成是单一BOUNDED CONTEXT中的完全CONTINUOUS INTEGRATION,还是集成度较轻的SHARED KERNEL或CUSTOMER/SUPPLIER DEVELOPER TEAM,或是单方面的CONFORMIST模式和防御型的ANTICORRUPTION LAYER模式。集成可能非常有价值,但它的代价也总是十分高昂的。我们应该确保在真正需要的地方进行集成。

14.9 模式:SEPARATE WAY

第14章 保持模型的完整性 - 图18

我们必须严格划定需求的范围。如果两组功能之间的关系并非必不可少,那么二者完全可以彼此独立。

集成总是代价高昂,而有时获益却很小。

除了在团队之间进行协调所需的常见开销以外,集成还迫使我们做出一些折中。可以满足某一特定需求的简单专用模型要为能够处理所有情况的更加抽象的模型让路。或许有些完全不同的技术能够轻而易举地提供某些特性,但它却难以集成。或许某个团队很难合作,使得其他团队在尝试与之合作时找不到行之有效的方法。

在很多情况下,集成不会提供明显的收益。如果两个功能部分并不需要互相调用对方的功能,或者这两个部分所使用的对象并不需要进行交互,或者在它们操作期间不共享数据,那么集成可能就是没有必要的(尽管可以通过一个转换层进行集成)。仅仅因为特性在用例中相关,并不一定意味着它们必须集成到一起。

因此:

声明一个与其他上下文毫无关联的BOUNDED CONTEXT,使开发人员能够在这个小范围内找到简单、专用的解决方案。

特性仍然可以被组织到中间件或UI层中,但它们将没有共享的逻辑,而且应该把通过转换层进行的数据传输减至最小,最好是没有数据传输。

示例 一个保险项目的简化

一个项目团队着手开发一个新的保险理赔软件,他们打算把客户服务代理或理赔人所需的一切功能都集成到一个系统中。经过一年的工作后,团队成员陷入僵局。分析瘫痪[2]再加上巨大的基础设施前期投资使他们在渐渐失去耐心的管理层面前没有任何可以展示的成果。更严重的是,他们尝试完成的工作规模将他们彻底压垮了。

新任项目经理把所有人员集中到一个房间中,让他们一周内制定一个新的计划。他们首先整理出需求列表,然后尝试估计它们的难度和重要性。他们坚决地删减掉那些困难并且不重要的需求。然后,开始为剩下的需求列表排列顺序。这个星期他们在这个房间里制定了很多明智的决策,但最后只有一个被证明是真正重要的。某个时候他们终于认识到有些特性几乎没有从集成得到任何好处。例如,理赔人需要访问一些现有数据库,而且他们目前的访问非常不方便。但是,尽管用户需要得到这些数据,但软件系统的其他特性却没有一个用到它们。

团队成员提出了各种简单的访问方式。一个提议是,可以把关键报告导出为HTML并放到内部网(intranet)上。另一个提议是,可以为理赔人提供一种专用查询,这种查询是用一个标准软件包编写的。通过在内部网的页面上放臵链接,或者在用户桌面上放臵按钮,就可以把所有这些功能集成进来。

团队启动了一组小项目,这些项目除了从同一个菜单启动之外,不再尝试任何集成。几个很有价值的功能几乎在一夜之间就完成了。卸去了这些过多特性的包袱之后,只剩下了一组精炼的需求,这使得主应用程序的交付又有了希望。

团队本来可以这样进行下去,但遗憾的是,他们又回到了老路,再次陷入困境。最后,只有那些采用SEPARATE WAY模式开发的小应用程序被证明是有用的。

采用SEPARATE WAY(各行其道)模式需要预先决定一些选项。尽管持续重构最后可以撤销任何决策,但完全隔离开发的模型是很难合并的。如果最终仍然需要集成,那么转换层将是必要的,而且可能很复杂。当然,不管怎样,这都是我们将要面对的问题。

现在,让我们回到更为合作的关系上,来看一下几种提高集成度的模式。

14.10 模式:OPEN HOST SERVICE

一般来说,在BOUNDED CONTEXT中工作时,我们会为CONTEXT外部的每个需要集成的组件定义一个转换层。当集成是一次性的,这种为每个外部系统插入转换层的方法可以以最小的代价避免破坏模型。但当子系统要与很多系统集成时,可能就需要更灵活的方法了。

当一个子系统必须与大量其他系统进行集成时,为每个集成都定制一个转换层可能会减慢团队的工作速度。需要维护的东西会越来越多,而且进行修改的时候担心的事情也会越来越多。

团队可能正在反复做着同样的事情。如果一个子系统有某种内聚性,那么或许可以把它描述为一组SERVICE,这组SERVICE满足了其他子系统的公共需求。

要想设计出一个足够干净的协议,使之能够被多个团队理解和使用,是一件十分困难的事情,因此只有当子系统的资源可以被描述为一组内聚的SERVICE并且必须进行很多集成的时候,才值得这样做。在这些情况下,它能够把维护模式和持续开发区别开。

因此:

定义一个协议,把你的子系统作为一组SERVICE供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。

这种通信形式暗含一些共享的模型词汇,它们是SERVICE接口的基础。这样,其他子系统就变成了与OPEN HOST(开放主机)的模型相连接,而其他团队则必须学习HOST团队所使用的专用术语。在一些情况下,使用一个众所周知的PUBLISHED LANGUAGE(公开发布的语言)作为交换模型可以减少耦合并简化理解。

14.11 模式:PUBLISHED LANGUAGE

两个BOUNDED CONTEXT之间的模型转换需要一种公共的语言。

当两个领域模型必须共存而且必须交换信息时,转换过程本身就可能很复杂,而且很难文档化和理解。如果正在构建一个新系统,我们一般会认为新模型是最好的,因此只考虑把其他模型转换成新模型就可以了。但有时我们的工作是增强一系列旧系统并尝试集成它们。这时要在众多模型中选择一个比较不烂的模型,也就是说“两害取其轻”。

另一种情况是,当不同业务之间需要互相交换信息时,应该如何做?想让一个业务采用另一个业务的领域模型不仅是不现实的,而且可能也不符合双方的需要。领域模型是为了解决其用户的需求而开发的,这样的模型所包含的一些特性可能使得与另一个系统的通信变得复杂,而实际上没有必要这么复杂。此外,如果把一个应用程序的模型用作通信媒介,那么它可能就无法为满足新需求而自由地修改了,它必须非常稳定,以便支持当前的通信职责。

与现有领域模型进行直接的转换可能不是一种好的解决方案。这些模型可能过于复杂或设计得较差。它们可能没有被很好地文档化。如果把其中一个模型作为数据交换语言,它实质上就被固定住了,而无法满足新的开发需求。

OPEN HOST SERVICE使用一个标准化的协议来支持多方集成。它使用一个领域模型来在各系统间进行交换,尽管这些系统的内部可能并不使用该模型。这里我们可以更进一步——发布这种语言,或找到一种已经公开发布的语言。我这里所说的发布仅仅是指该语言已经可以供那些对它感兴趣的群体使用,而且已经被充分文档化,兼容一些独立的解释。

最近,电子商务界出现了一种激动人心的新技术:XML(可扩展标记语言)。这种技术有望使数据交换变得更加容易。XML的一个非常有价值的特性是通过DTD(文档类型定义)或XML模式来正式定义一个专用的领域语言,从而使得数据可以被转换为这种语言。一些行业组织已经成立,准备为各自的行业定义一种标准的DTD,这样,业内多方就可以交换信息了,如交换化学公式信息或遗传代码信息。实际上这些组织正在以语言定义的形式创建一种共享的领域模型。

因此:

把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。

这种语言不必从头创建。很多年以前,我曾经受聘于一家公司,这家公司有一个用Smalltalk编写的软件产品,它使用DB2存储数据。公司希望灵活地把软件分发给那些没有DB2许可的用户,于是请我为Btrieve创建一个接口,Btrieve是一个轻量级的数据库引擎,它有一个免费的运行时分发许可。Btrieve并不完全是关系型的,但我的客户只用到DB2的很小的一部分功能,而且两个数据库都能提供这种能力。公司的开发人员已经在DB2之上建立了某种存储对象的抽象,于是我决定把这些工作作为我的Btrieve组件的接口。

这种方法确实很有效。软件顺利地与我的客户系统集成到一起。但是,客户设计中缺少有关持久化对象的抽象的正式规格说明或文档,这意味着我必须做很多工作来确定新组件的需求。此外,不太可能重用该组件把其他应用程序从DB2迁移到Btrieve。而且新软件稳固了公司的持久化模型,使得持久化对象模型的重构变得更困难。

更好的方法可能是标识出公司所使用的那一小部分DB2接口,然后为其提供支持就可以了。DB2的接口由SQL和大量专有协议构成。尽管接口很复杂,但它已经被严格地规定并充分文档化。由于公司只使用接口的一个很小的子集,因此复杂性有所降低。如果已开发出一个模拟必要的DB2 接口子集的组件,那么开发人员所需做的文档化工作只是标识出该子集即可。与之集成的应用程序已经知道如何与DB2对话,因此额外要做的工作很少。将来重新设计持久层的工作仅限于DB2子集的使用,就像前面做的改进一样。

DB2接口是PUBLISHED LANGUAGE的一个例子。在这个例子中,两个模型都不属于业务领域,但它们所应用的原则是一致的。由于协作中的一个模型已经是一种PUBLISHED LANGUAGE,因此就不需要引入第三方语言了。

示例 一种化学的PUBLISHED Language

在工业界和学术界,有无数的程序用于分类、分析和处理化学公式。几乎每个程序都使用不同的领域模型来表示化学结构,因此数据的交换总是很难。当然,大部分程序都是用一些无法充分表达领域模型的语言编写的(如FORTRAN)。当有人想要共享数据时,他们不得不先了解其他系统的数据库的细节,然后再研究出某种转换方案。

CML(化学标记语言)正是在这种背景下诞生的,它是作为化学领域的公共交流语言被开发出来的专用XML,由一个代表学术界和工业界的组织负责开发和管理[Murray-Rust et al.1995]。

化学信息非常复杂和多样化,而且会随着新发现而不断变化。因此,该组织开发了一种用于描述基础知识的语言,如有机和无机分子的化学公式、蛋白质序列、光谱或物理量。

既然这种语言已经公开发布,人们就可以开发相应的工具了(以前,要开发这样的工具是不值得的,因为它们只能用于一种数据库)。例如,人们开发一种名为JUMBO Browser的Java应用程序,它的功能是为那些以CML格式存储的化学结构创建图形视图。因此,如果你的数据采用了CML格式,就可以使用这样的可视化工具。

事实上,CML通过使用XML(一种已发布的元语言)获得了双重优势。一个优势是人们对XML很熟悉,因此很容易学习CML,另一个优势是由于有大量现成的工具(如解析器),因此CML的实现很容易,而且有大量书籍介绍了XML的各个方面,这对CML的文档化有很大帮助。

下面是一个CML的小例子。虽然像我这样的外行并不能清楚地理解它是什么意思,但它的原则还是很清晰的。

第14章 保持模型的完整性 - 图19

14.12 “大象”的统一

六个好学的古印度人,

一起去看大象,

(他们都是盲人),

都通过触摸,

来满足了解事物的心愿。

第一个接近大象的盲人,

恰巧了撞上了大象宽阔结实的身躯,

马上叫到:“上帝保佑,原来大象就像一堵墙。”

……

第三个盲人,

碰巧把扭动着的象鼻抓在手中,因此就大胆地说道:

“依我看,大象就像一条蛇!”

第四个盲人急切地伸出双手,

摸到了大象的膝盖,

“这头奇异的怪兽最像什么已经很明显了”,他说,

“很明显,大象就像一棵树”

……

第六个盲人一开始摸这头大象,

就抓住了它摆动着的尾巴,

他说,“我认为大象就像一根绳子!”

这六个印度人,

大声地争论个不停,

他们每个人的观点,

都过于僵化和固执,

尽管他们每人都有正确的地方,

但从整体上都是错误的!

……

——摘自John Godfrey Saxe(1816—1887)创作的《盲人与象》,来源于印度自说经[3]Udana中的故事

即便他们对大象的本质不能达成完全的一致,这些盲人仍然可以根据他们所触摸到的大象身体的部位来扩展各自的认识。如果并不需要集成,那么模型统不统一就无关紧要。如果他们需要进行一些集成,那么实际上并不需要对大象是什么达成一致,而只要接受各种不同意见就会获得很多价值。这样,他们就不会在不知不觉中各执己见。

第14章 保持模型的完整性 - 图20 图14-9 4个没有集成的上下文

上图用UML图表示了6个盲人所认识到的大象模型。这张图建立了4个独立的BOUNDED CONTEXT,情况很明显,他们必须找到一种方式来交流他们共同关心的少数几个方面,或许他们共同关心的就是大象所在的位臵。

当盲人想要分享更多有关大象的信息时,他们会从共享单个BOUNDED CONTEXT得到更大的价值。但统一不同的模型却很难做到。可能没有人愿意放弃自己的模型而采用别人的模型。毕竟,摸到尾巴的那个人知道大象并不像一颗树,而且那个模型对他来说没有意义,也没有用处。统一多个模型几乎总是意味着创建一个新模型。

第14章 保持模型的完整性 - 图21 图14-10 4个只有最小集成的上下文

经过一些想象和讨论(也许是激烈的讨论)之后,盲人们最终可能会认识到他们正在对一个更大整体的不同部分进行描述和建模。从很多方面来讲,部分—整体的统一可能不需要花费很多工作。至少集成的第一步只需弄清楚各个部分是如何相连的就够了。可以把大象看成一堵墙,下面通过树干支撑着,一头儿是一根绳子,另一头儿是一条蛇,这样看就可以适当地满足一些需求了。

第14章 保持模型的完整性 - 图22 图14-11 一个粗略集成的上下文

大象模型的统一要比大多数这样的合并相对简单一些。遗憾的是,大象模型的统一只是一个特例——不同模型纯粹是在描述整体的不同部分,然而,这通常是模型之间差别的一个方面而已。当两个模型以不同方式描述同一部分时,问题会变得更加困难。如果两个盲人都摸到了象鼻子,一个人认为它像蛇,而另一个人认为它像消防水管,那么他们将更难集成。双方都无法接受对方的模型,因为那不符合自己的体验。事实上,他们需要一个新的抽象,这个抽象需要把蛇的“活着的特性”与消防水管的喷水功能合并到一起,而这个抽象还应该排除先前两个模型中的一些不确切的含义,如人们可能会想到的毒牙,或者可以从身体上拆下并卷起来放到救火车中的这种性质。

尽管我们已经把部分合并成一个整体,但得到的模型还是很简陋的。它缺乏内聚性,也没有形成任何潜在领域的轮廓。在持续精化的过程中,新的理解可能会产生更深层的模型。新的应用程序需求也可能会促成更深层的模型。如果大象开始移动了,那么“树”理论就站不住脚了,而盲人建模者们也可能会有所突破,形成“腿”的概念。

第14章 保持模型的完整性 - 图23 图14-12 一个更深入集成的上下文

模型集成的第二步是去掉各个模型中那些偶然或不正确的方面,并创建新的概念,在本例中,这个概念就是一种“动物”,它长着“鼻子”、“腿”、“身体”和“尾巴”,每个部分都有其自己的属性以及与其他部分的明确关系。在很大程度上,成功的模型应该尽可能做到精简。象鼻与蛇相比,其特性和功能可能比蛇多,也可能比蛇少,但宁“少”勿“多”。宁可缺少喷水功能,也不要包含不正确的毒牙特性。

如果目标只是找到大象,那么只要对每个模型中所表示的位臵进行转换就可以了。当需要更多集成时,第一个版本的统一模型不一定达到完全的成熟。把大象看成一堵墙,下面用树干支撑着,一头儿是一根绳子,另一头儿是一条蛇,就可以适当地满足一些需求了。紧接着,通过新需求和进一步的理解及沟通的推动,模型可以得到加深和精化。

承认多个互相冲突的领域模型实际上正是面对现实的做法。通过明确定义每个模型都适用的上下文,可以维护每个模型的完整性,并清楚地看到要在两个模型之间创建的任何特殊接口的含义。盲人没办法看到整个大象,但只要他们承认各自的理解是不完整的,他们的问题就能得到解决。

14.13 选择你的模型上下文策略

在任何时候,绘制出CONTEXT MAP来反映当前状况都是很重要的。但是,一旦绘制好CONTEXT MAP之后,你很可能想要改变现状。现在,你可以开始有意识地选择CONTEXT的边界和关系。以下是一些指导原则。

14.13.1 团队决策或更高层决策

首先,团队必须决定在哪里定义BOUNDED CONTEXT,以及它们之间有什么样的关系。这些决策必须由团队做出,或者至少传达给整个团队,并且被团队里的每个人理解。事实上,这样的决策通常需要与外部团队达成一致。按照本身价值来说,在决定是否扩展或分割BOUNDED CONTEXT时,应该权衡团队独立工作的价值以及能产生直接且丰富集成的价值,以这两种价值的成本—效益作为决策的依据。在实践中,团队之间的行政关系往往决定了系统的集成方式。由于汇报结构,有技术优势的统一可能无法实现。管理层所要求的合并可能并不实用。你不会总能得到你想要的东西,但你至少可以评估出这些决策的代价,并反映给管理层,以便采取相应的措施来减小代价。从一个现实的CONTEXT MAP开始,并根据实际情况来选择改变。

14.13.2 臵身上下文中

开发软件项目时,我们首先是对自己团队正在开发的那些部分感兴趣(“设计中的系统”),其次是对那些与我们交互的系统感兴趣。典型情况下,设计中的系统将被划分为一到两个BOUNDED CONTEXT,开发团队的主力将在这些上下文中工作,或许还会有另外一到两个起支持作用的CONTEXT。除此之外,就是这些CONTEXT与外部系统之间的关系。这是一种简单、典型的情况,能让你对可能会遇到的情形有一些粗略的了解。

实际上,我们正是自己所处理的主要CONTEXT的一部分,这会在我们的CONTEXT MAP中反映出来。只要我们知道自己存在偏好,并且在超出该CONTEXT MAP的应用边界时能够意识到已越界,那么就不会有什么问题。

14.13.3 转换边界

在画出BOUNDED CONTEXT的边界时,有无数种情况,也有无数种选择。但权衡时所要考虑的通常是下面所列出的某些因素。

首选较大的BOUNDED CONTEXT

当用一个统一模型来处理更多任务时,用户任务之间的流动更顺畅。

一个内聚模型比两个不同模型再加它们之间的映射更容易理解。

两个模型之间的转换可能会很难(有时甚至是不可能的)。

共享语言可以使团队沟通起来更清楚。

首选较小的BOUNDED CONTEXT

开发人员之间的沟通开销减少了。

由于团队和代码规模较小,CONTINUOUS INTEGRATION更容易了。

较大的上下文要求更加通用的抽象模型,而掌握所需技巧的人员会出现短缺。

不同的模型可以满足一些特殊需求,或者是能够把一些特殊用户群的专门术语和UBIQUITOUS LANGUAGE的专门术语包括进来。

在不同BOUNDED CONTEXT之间进行深度功能集成是不切实际的。在一个模型中,只有那些能够严格按照另一个模型来表述的部分才能够进行集成,而且,即便是这种级别的集成可能也需要付出相当大的工作量。当两个系统之间有一个很小的接口时,集成是有意义的。

14.13.4 接受那些我们无法更改的事物:描述外部系统

最好从一些最简单的决策开始。一些子系统显然不在开发中的系统的任何BOUNDED CONTEXT中。一些无法立即淘汰的大型遗留系统和那些提供所需服务的外部系统就是这样的例子。我们很容易就能识别出这些系统,并把它们与你的设计隔离开。

在做出假设时必须要保持谨慎。我们会很轻易地认为这些系统构成了其自己的BOUNDED CONTEXT,但大多数外部系统只是勉强满足定义。首先,定义BOUNDED CONTEXT的目的是把模型统一在特定边界之内。你可能负责遗留系统的维护,在这种情况下,可以明确地声明这一目的,或者也可以很好地协调遗留团队来执行非正式的CONTINUOUS INTEGRATION,但不要认为遗留团队的配合是理所当然的事情。仔细检查,如果开发工作集成得不好,一定要特别小心。在这样的系统中,不同部分之间出现语义矛盾是很平常的事情。

14.13.5 与外部系统的关系

这里可以应用3种模式。首先,可以考虑SEPARATE WAY模式。当然,如果你不需要集成,就不用把它们包括进来。但一定要真正确定不需要集成。只为用户提供对两个系统的简单访问确实够用吗?集成要花费很大代价而且还会分散精力,因此要尽可能为你的项目减轻负担。

如果集成确实非常重要,可以在两种极端的模式之中进行选择:CONFORMIST模式或ANTICORRUPTION LAYER模式。作为CONFORMIST并不那么有趣,你的创造力和你对新功能的选择都会受到限制。当构建一个大型的新系统时,遵循遗留系统或外部系统的模型可能是不现实的(毕竟,为什么要构建新系统呢?)。但是,当对一个大的系统进行外围扩展时,而且这个系统仍然是主要系统,在这种情况下,继续使用遗留模型可能就很合适。这种选择的例子包括轻量级的决策支持工具,这些工具通常是用Excel或其他简单工具编写的。如果你的应用程序确实是现有系统的一个扩展,而且与该系统的接口很大,那么CONTEXT之间转换所需的工作量可能比应用程序功能本身需要的工作量还大。尽管你已经处于另一个系统的BOUNDED CONTEXT中,但你自己的一些好的设计仍然有用武之地。如果另一个系统有着可以识别的领域模型,那么只要使这个模型比在原来的系统中更清晰,你就可以改进你的实现,唯一需要注意的是要严格地遵照那个老模型。如果你决定采用CONFORMIST设计,就必须全心全意地去做。你应该约束自己只可以去扩展现有模型,而不能去修改它。

当正在设计的系统功能并不仅仅是扩展现有系统时,而且你与另一个系统的接口很小,或者另一个系统的设计非常糟糕,那么实际上你会希望使用自己的BOUNDED CONTEXT,这意味着需要构建一个转换层,甚至是一个ANTICORRUPTION LAYER。

14.13.6 设计中的系统

你的项目团队正在构建的软件就是设计中的系统。你可以在这个区域内声明BOUNDED CONTEXT,并在每个BOUNDED CONTEXT中应用CONTINUOUS INTEGRATION,以便保持它们的统一。但应该有几个上下文呢?各个上下文之间又应该是什么关系呢?与外部系统的情况相比,这些问题的答案会变得更加不确定,因为我们拥有更多的主动权。

情况可能非常简单:设计中的整个系统使用一个BOUNDED CONTEXT。例如,当一个少于10人的团队正在开发高度相关的功能时,这可能就是一种很好的选择。

随着团队规模的增大,CONTINUOUS INTEGRATION可能会变得困难起来(尽管我也曾看到过一些较大的团队仍能保持持续集成)。你可能希望采用SHARED KERNEL模式,并把几组相对独立的功能划分到不同的BOUNDED CONTEXT中,使得在每个BOUNDED CONTEXT中工作的人员少于10人。在这些BOUNDED CONTEXT中,如果有两个上下文之间的所有依赖都是单向的,就可以建成CUSTOMER/SUPPLIER DEVELOPMENT TEAM。

你可能认识到两个团队的思想截然不同,以致他们的建模工作总是发生矛盾。可能他们需要从模型得到完全不同的东西,或者只是背景知识有某种不同,又或者是由于项目所采用的管理结构而引起的。如果这种矛盾的原因是你无法改变或不想改变的,那么可以让他们的模型采用SEPARATE WAY模式。在需要集成的地方,两个团队可以共同开发并维护一个转换层,把它作为唯一的CONTINUOUS INTEGRATION点。这与同外部系统的集成正好相反,在外部集成中,一般由ANTICORRUPTION LAYER来起调节作用,而且从另一端得不到太多的支持。

一般来说,每个BOUNDED CONTEXT对应一个团队。一个团队也可以维护多个BOUNDED CONTEXT,但多个团队在一个上下文中工作却是比较难的(虽然并非不可能)。

14.13.7 用不同模型满足特殊需要

同一业务的不同小组常常有各自的专用术语,而且可能各不相同。这些本地术语可能是非常精确的,并且是根据他们的需要定制的。要想改变它们(例如,施行标准化的企业级术语),需要大量的培训和分析,以便解决差异问题。即使如此,新术语仍然可能没有原来那个已经经过精心调整的术语好用。

你可能决定通过不同的BOUNDED CONTEXT来满足这些特殊需要,除了转换层的CONTINUOUS INTEGRATION以外,让模型采用SEPARATE WAY模式。UBIQUITOUS LANGUAGE的不同专用术语将围绕这些模型以及它们所基于的行话来发展。如果两种专用术语有很多重叠之处,那么SHARED KERNEL模式就可以满足特殊化要求,同时又能把转换成本减至最小。

当不需要集成或者集成相对有限时,就可以继续使用已经习惯的术语,以免破坏模型。但这也有其自己的代价和风险。如下所示。

没有共同的语言,交流将会减少。

集成开销更高。

随着相同业务活动和实体的不同模型的发展,工作会有一定的重复。

但是,最大的风险或许是,它会成为拒绝改变的理由,或为古怪、狭隘的模型辩护。为了满足特殊的需要,需要对系统的这一部分进行多大的定制?最重要的是,这个用户群的专门术语有多大的价值?你必须在团队独立操作的价值与转换的风险之间做出权衡,并且留心合理地处理一些没有价值的术语变化。

有时会出现一个深层次的模型,它把这些不同语言统一起来,并能够满足双方的要求。只有经过大量开发工作和知识消化之后,深层次模型才会在生命周期的后期出现。深层次模型不是计划出来的,我们只能在它出现的时候抓住机遇,修改自己的策略并进行重构。

记住,在需要大量集成的地方,转换成本会大大增加。在团队之间进行一些协调工作(从精确地修改一个具有复杂转换的对象到采用SHARED KERNEL模式)可以使转换变得更加容易,同时又不需要完全的统一。

14.13.8 部署

在复杂系统中,对打包和部署进行协调是一项繁琐的任务,这类任务总是要比看上去难得多。BOUNDED CONTEXT策略的选择将影响部署。例如,当CUSTOMER/SUPPLIER TEAM部署新版本时,他们必须相互协调来发布经过共同测试的版本。在这些版本中,必须要进行代码和数据迁移。在分布式系统中,一种好的做法是把CONTEXT之间的所有转换层放在同一个进程中,这样就不会出现多个版本共存的情况。

当数据迁移可能很花时间或者分布式系统无法同步更新时,即使是单一BOUNDED CONTEXT中的组件部署也是很困难的,这会导致代码和数据有两个版本共存。

由于部署环境和技术存在不同,有很多技术因素需要考虑。但BOUNDED CONTEXT关系可以为我们指出重点问题。转换接口已经被标出。

绘制CONTEXT边界时应该反映出部署计划的可行性。当两个CONTEXT通过一个转换层连接时,要想更新其中的一个CONTEXT,新的转换层需要为另一个CONTEXT提供相同的接口。SHARED KERNEL需要进行更多的协调工作,不仅在开发中如此,而且在部署中也同样应该如此。SEPARATE WAY模式可以使工作简单很多。

14.13.9 权衡

通过总结这些指导原则可知有很多统一或集成模型的策略。一般来说,我们需要在无缝功能集成的益处和额外的协调和沟通工作之间做出权衡。还要在更独立的操作与更顺畅的沟通之间做出权衡。更积极的统一需要对有关子系统的设计有更多控制。

第14章 保持模型的完整性 - 图24 图14-13 CONTEXT关系模式的相对要求

14.13.10 当项目正在进行时

很多情况下,我们不是从头开发一个项目,而是会改进一个正在开发的项目。在这种情况下,第一步是根据当前的状况来定义BOUNDED CONTEXT。这很关键。为了有效地定义上下文,CONTEXT MAP必须反映出团队的实际工作,而不是反映那个通过遵守以上描述的指导原则而得出的理想组织。

描述了当前真实的BOUNDED CONTEXT以及它们的关系以后,下一步就是围绕当前组织结构来加强团队的工作。在CONTEXT中加强CONTINUOUS INTEGRATION。把所有分散的转换代码重构到ANTICORRUPTION LAYER中。命名现有的BOUNDED CONTEXT,并确保它们处于项目的UBIQUITOUS LANGUAGE中。

现在可以开始考虑修改边界和它们的关系了。这些修改很自然地由相同的原则来驱动——之前已经描述了在新项目上使用这些原则,但我们应该把这些修改分成较小的部分,以便根据实际情况做出选择,从而在只花费最少的工作和对模型产生最小破坏的前提下创造最大的价值。

下一节将讨论如何修改CONTEXT的边界。

14.14 转换

像建模和设计的其他方面一样,有关BOUNDED CONTEXT的决策并非不可改变的。在很多情况下,我们必须改变最初有关边界以及BOUNDED CONTEXT之间关系的决策,这是不可避免的。一般而言,分割CONTEXT是很容易的,但合并它们或改变它们之间的关系却很难。下面将介绍几种有代表性的修改,它们很难,但也很重要。这些转换往往很大,无法在一次重构中完成,甚至无法在一次项目迭代中完成。因为这个原因,我将把这些转换划分为一系列简单的步骤。当然,这些只是一些指导原则,你必须根据你的特殊情况和事件对它们进行调整。

14.14.1 合并CONTEXT:SEPARATE WAY →SHARED KERNEL

合并BOUNDED CONTEXT的动机很多:翻译开销过高、重复现象很明显。合并很难,但什么时候做都不晚,只是需要一些耐心。

即使你的最终目标是完全合并成一个采用CONTINUOUS INTEGRATION的CONTEXT,也应该先过渡到SHARED KERNEL。

(1) 评估初始状况。在开始统一两个CONTEXT之前,一定要确信它们确实需要统一。

(2) 建立合并过程。你需要决定代码的共享方式以及模块应该采用哪种命名约定。SHARED KERNEL的代码至少每周要集成一次,而且它必须有一个测试套件。在开发任何共享代码之前,先把它设臵好。(测试套件将是空的,因此很容易通过!)

(3) 选择某个小的子领域作为开始,它应该是两个CONTEXT中重复出现的子领域,但不是CORE DOMAIN的一部分。最初的合并主要是为了建立合并过程,因此最好选择一些简单且相对通用或不重要的部分。检查已存在的集成和转换。选择那些经过转换的部分,其优势在于一开始就有用于验证的转换机制,此外还可以简化转换层。

此时,我们有两个应对相同子领域的模型。基本上有3种合并方法。我们可以选择一个模型,并重构另一个CONTEXT,使之与第一个模型兼容。我们可以从整体上做出这个决策,把目标设臵为系统性地替换一个CONTEXT的模型,并保持被开发模型的内聚性。也可以一次选择一部分,到最后两个模型可能会“两全其美”(但注意最后不要弄得一团糟)。

第三种选择是找到一个新模型,这个模型可能比最初的两个都深刻,能够承担二者的职责。

(4) 从两个团队中共选出2~4位开发人员组成一个小组,由他们来为子领域开发一个共享的模型。不管模型是如何得出的,它的内容必须详细。这包括一些困难的工作:识别同义词和映射那些尚未被翻译的术语。这个联合团队需要为模型开发一个基本的测试集。

(5) 来自两个团队的开发人员一起负责实现模型(或修改要共享的现有代码)、确定各种细节并使模型开始工作。如果这些开发人员在模型中遇到了问题,就从第(3)步开始重新组织团队,并进行必要的概念修订工作。

(6) 每个团队的开发人员都承担与新的SHARED KERNEL集成的任务。

(7) 清除那些不再需要的翻译。

这时你会得到一个非常小的SHARED KERNEL,并且有一个过程来维护它。在后续的项目迭代中,重复第(3)~(7)步来共享更多内容。随着过程的不断巩固和团队信心的树立,就可以选择更复杂的子领域了,同时处理多个子领域,或者处理CORE DOMAIN中的子领域。

注意:当从模型中选取更多与领域有关的部分时,可能会遇到这样的情况,即两个模型各自采用了不同用户群的专用术语。聪明的做法是先不要把它们合并到SHARED KERNEL中,除非工作中出现了突破,得到了一个深层模型,这个模型为你提供了一种能够替代那两种专用术语的语言。SHARED KERNEL的优点是它具有CONTINUOUS INTEGRATION的部分优势,同时又保留了SEPARATE WAY模式的一些优点。

以上这些是把模型的一些部分合并到SHARED KERNEL中的指导原则。在继续讨论之前,我们来看一下另外一种方法,它能够部分解决上述转换所面对的问题。如果两个模型中有一个毫无疑问是符合首选条件的,那么就考虑向它过渡,而不用进行集成。不共享公共的子领域,而只是系统性地通过重构应用程序把这些子领域的所有职责从一个BOUNDED CONTEXT转移到另一个BOUNDED CONTEXT,从而使用那个更受青睐的CONTEXT的模型,并对该模型进行需要的增强。在没有集成开销的情况下,消除了冗余。很有可能(但也不是必然的)那个更受青睐的BOUNDED CONTEXT最终会完全取代另一个BOUNDED CONTEXT,这样就实现了与合并完全一样的效果。在转换过程中(这个过程可能相当长或无法确定),这种方法具有SEPARATE WAY模式常见的优点和缺点,而且我们必须拿这些优缺点与SHARED KERNEL的利弊进行权衡。

14.14.2 合并CONTEXT:SHARED KERNEL→CONTINUOUS INTEGRATION

如果你的SHARED KERNEL正在扩大,你可能会被完全统一两个BOUNDED CONTEXT的优点所吸引。但这并不只是一个解决模型差异的问题。你将改变团队的结构,而且最终会改变人们所使用的语言。

这个过程从人员和团队的准备开始。

(1) 确保每个团队都已经建立了CONTINUOUS INTEGRATION所需的所有过程(共享代码所有权、频繁集成等)。两个团队协商集成步骤,以便所有人都以同一步调工作。

(2) 团队成员在团队之间流动。这样可以形成一大批同时理解两个模型的人员,并且可以把两个团队的人员联系起来。

(3) 澄清每个模型的精髓(参见第15章)。

(4) 现在,团队应该有了足够的信心把核心领域合并到SHARED KERNEL中。这可能需要多次迭代,有时需要在新共享的部分与尚未共享的部分之间使用临时的转换层。一旦进入到合并CORE DOMAIN的过程中,最好能快速完成。这是一个开销高且易出错的阶段,因此应该尽可能缩短时间,要优先于新的开发任务。但注意量力而行,不要超过你的处理能力。

有几种方式用于合并CORE模型。可以保持一个模型,然后修改另一个,使之与第一个兼容,或者可以为子领域创建一个新模型,并通过修改两个上下文来使用这个模型。如果两个模型已经被修改以满足不同用户的需要,你就要注意了。你需要保留两个初始模型中的这些专业能力。这就要求开发一个能够替代两个原始模型的更深层的模型。开发这样一个更深入的统一模型是很难的,但如果你已经决定完全合并两个CONTEXT,就没有选择多种专门术语的空间了。这样做的好处是最终模型和代码的集成变得更清晰了。注意不要影响到你满足用户特殊需要的能力。

(5) 随着SHARED KERNEL的增长,把集成频率提高到每天一次,最后实现CONTINUOUS INTEGRATION。

(6) 当SHARED KERNEL逐渐把先前两个BOUNDED CONTEXT的所有内容都包括进来的时候,你会发现要么形成了一个大的团队,要么形成了两个较小的团队,这两个较小的团队共享一个CONTINUOUS INTEGRATION的代码库,而且团队成员可以经常在两个团队之间来回流动。

14.14.3 逐步淘汰遗留系统

好花美丽不常开,好景怡人不常在,就算遗留计算机软件也一样会走向终结。但这可不会自动自发地出现。这些老的系统可能与业务及其他系统紧密交织在一起,因此淘汰它们可能需要很多年。好在我们并不需要一次就把所有东西都淘汰掉。

这一话题的涉及面太广了,这里的讨论也只能浅尝辄止。我们将讨论一种常见的情况:用一系列更现代的系统来补充业务中每天都在使用的老系统,新系统通过一个ANTICORRUPTION LAYER与老系统进行通信。

首先要执行的步骤是确定测试策略。应该为新系统中的新功能编写自动的单元测试,但逐步淘汰遗留系统还有一些特殊的测试需求。一些组织在某段时间内会同时运行新旧两个系统。

在任何一次迭代中:

(1) 确定遗留系统的哪个功能可以在一个迭代中被添加到某个新系统中;

(2) 确定需要在ANTICORRUPTION LAYER中添加的功能;

(3) 实现;

(4) 部署;

有时,需要进行多次迭代才能编写一个与遗留系统的某个功能等价的功能单元,这时在计划新的替代功能时仍以小规模的迭代为单元,最后一次性部署多次迭代。

部署涉及的变数太多,以至于我不可能涵盖所有的基本情况。就开发而言,如果这些小规模、增量的改动能够推到生产环境,那真是再好不过了。但通常情况,还是需要将他们组织成更大的发布。在新软件的使用方面,用户培训是必不可少的。有时在成功部署的同时还必须进行开发工作。还有很多后勤问题需要解决。

一旦最终进入运行阶段后,应该遵循如下步骤。

(5) 找出ANTICORRUPTION LAYER中那些不必要的部分,并去掉它们;

(6) 考虑删除遗留系统中目前未被使用的模块,虽然这种做法未必实际。有趣的是,遗留系统设计得越好,它就越容易被淘汰。而设计得不好的软件却很难一点儿一点儿地去除。这时,我们可以暂时忽略那些未使用的部分,直到将来剩余部分已经被淘汰,这时整个遗留系统就可以停止使用了。

不断重复这几个步骤。遗留系统应该越来越少地参与业务,最终,替换工作会看到希望的曙光并完全停止遗留系统。同时,随着各种组合增加或减小系统之间的依赖,ANTICORRUPTION LAYER将相应地收缩或扩张。当然,在其他条件都相同的情况下,应该首先迁移那些只产生较小ANTICORRUPTION LAYER的功能。但其他因素也可能会起主导作用,有时候在过渡期间可能必须经历一些麻烦的转换。

14.14.4 OPEN HOST SERVICE→PUBLISHED LANGUAGE

我们已经通过一系列特定的协议与其他系统进行了集成,但随着需要访问的系统逐渐增多,维护负担也不断增加,或者交互变得很难理解。我们需要通过PUBLISHED LANGUAGE来规范系统之间的关系。

(1) 如果有一种行业标准语言可用,则尽可能评估并使用它。

(2) 如果没有标准语言或预先公开发布的语言,则完善作为HOST的系统的CORE DOMAIN(参见第15章)。

(3) 使用CORE DOMAIN作为交换语言的基础,尽可能使用像XML这样的标准交互范式。

(4)(至少)向所有参与协作的各方发布新语言。

(5) 如果涉及新的系统架构,那么也要发布它。

(6) 为每个协作系统构建转换层。

(7) 切换。

现在,当加入更多协作系统时,对整个系统的破坏已经减至最小了。

记住,PUBLISHED LANGUAGE必须是稳定的,但是当继续进行重构时,仍然需要能够自由地更改HOST的模型。因此,不要把交换语言和HOST的模型等同起来。保持它们的密切关系可以减小转换开销,而你的HOST可以采用CONFORMIST模式。但是应该保留对转换层进行补充的权力,在成本—效益的折中需要时,可以把这个权利分离出去。

项目领导者应该根据功能集成需求和开发团队之间的关系来定义BOUNDED CONTEXT。一旦BOUNDED CONTEXT和CONTEXT MAP被明确地定义下来并获得认可,就应该保持它们的逻辑一致性。最起码要把相关的通信问题提出来,以便解决它们。

但是,有时模型上下文(无论是我们有意识地划定边界的还是自然出现的上下文)被错误地用来解决系统中的一些其他问题,而不是逻辑不一致问题。团队可能会发现一个很大的CONTEXT的模型由于过于复杂而无法作为一个整体来理解或透彻地分析。出于有意或无意的考虑,团队往往会把CONTEXT分割为更易管理的部分。这种分割会导致失去很多机会。现在,值得花费一些功夫仔细考查在一个大的CONTEXT中建立一个大模型的决策了。如果从组织结构或行政角度来看保持一个大模型并不现实,如果实际上模型就是分裂的,那么就重新绘制上下文图,并定义能够保持的边界。但是,如果保持一个大的BOUNDED CONTEXT能够解决迫切的集成需要,而且除了模型本身的复杂性以外,这看上去是行得通的,那么分割CONTEXT可能就不是最佳的选择了。

在做出这种牺牲之前,还应该考虑其他一些能够使大模型变得易于管理的方法。下两章将着重讨论通过应用两种更广泛的原则(精炼和大型结构)来管理大模型的复杂性。