战略设计(strategic design)——一种针对系统整体的建模和设计决策。这样的决策影响整个项目,而且必须由团队来制定。
随着系统的增长,它会变得越来越复杂,当我们无法通过分析对象来理解系统的时候,就需要掌握一些操纵和理解大的模型的技术了。本文将介绍一些原则。遵循这些原则,就可以对非常复杂的领域进行建模。大部分这样的决策都需要由团队来制定,甚至需要多个团队共同协商制定。这些决策往往是把设计和策略综合到一起的结果。
最负雄心的企业系统意欲实现一个涵盖所有业务、紧密集成的系统。然而在几乎所有这种规模的组织中,整体业务模型太大也太复杂了,因此难以管理,甚至很难把它作为一个整体来理解。我们必须在概念和实现上把系统分解为较小的部分。但问题在于,如何保证实现这种模块化的同时,不失去集成所具备的好处;从而使系统的不同部分能够进行互操作,以便协调各种业务操作。如果设计一个把所有概念都涵盖进来的单一领域模型,它将会非常笨拙,而且将会出现大量难以察觉的重复和矛盾。而如果用临时拼凑的接口把一组小的、各自不同的子系统集成到一起,又不具备解决企业级问题的能力,并且在每个集成点上都有可能出现不一致。通过采用系统的、不断演变的设计策略,就可以避免这两种极端问题。
即使在这种规模的系统中采用领域驱动设计方法,也不要脱离实现去开发模型。**每个决策都必须对系统开发产生直接的影响,否则它就是无关的决策。**战略设计原则必须指导设计决策,以便减少各个部分之间的互相依赖,在使设计意图更为清晰的同时而又不失去关键的互操作性和协同性。**战略设计原则必须把模型的重点放在捕获系统的概念核心,也就是系统的"远景"上。**而且在完成这些目标的同时又不能为项目带来麻烦。为了帮助实现这些目标,这一部分探索了3个大的主题:上下文、精炼和大型结构。
其中上下文是最不易引起注意的原则,但实际上它却是最根本的。无论大小,成功的模型必须在逻辑上一致,不能有互相矛盾或重叠的定义。有时,企业系统会集成各种不同来源的子系统,或包含诸多完全不同的应用程序,以至于无法从同一个角度来看待领域。要把这些不同部分中隐含的模型统一起来可能是要求过高了。通过为每个模型显式地定义一个BOUNDED CONTEXT,然后在必要的情况下定义它与其他上下文的关系,建模人员就可以避免模型变得缠杂不清。
通过精炼可以减少混乱,并且把注意力集中到正确的地方。人们通常在领域的一些次要问题上花费了太多的精力。整体领域模型需要突出系统中最有价值和最特殊的那些方面,而且在构造领域模型时应该尽可能把注意力集中在这些部分上。虽然一些支持组件也很关键,但绝不能把它们和领域核心一视同仁。把注意力集中到正确的地方不仅有助于把精力投入到关键部分上,而且还可以使系统不会偏离预期方向。战略精炼可以使大的模型保持清晰。有了更清晰的视图后,CORE DOMAIN的设计就会发挥更大的作用。
大型结构是用来描述整个系统的。在非常复杂的模型中,人们可能会"只见树木,不见森林"。精炼确实有帮助,它使人们能够把注意力集中到核心元素上,并把其他元素表示为支持作用,但如果不贯彻某个主旨来应用一些系统级的设计元素和模式的话,关系仍然可能非常混乱。本文将概要介绍几种大型结构方法,然后详细讨论其中一种模式——RESPONSIBILITY LAYER(职责层),通过这个示例来探索使用大型结构的含义。我们所讨论的特殊结构只是一些例子,它们并不是大型结构的全部。当需要的时候,应该创造新的结构,抑或修改这些结构,但均需遵循演化顺序(EVOLVING ORDER)的过程来进行。一些大型结构能够使设计保持一致性,从而加速开发,并提高集成度。
这3种原则各有各的用处,但结合起来使用将发挥更大的力量,遵守这些原则就可以创建出好的设计,即使是对一个非常庞大的没有人能够完全理解的系统也是如此。大型结构能够保持各个不同部分之间的一致性,从而有助于这些部分的集成。结构和精炼能够帮助我们理解各个部分之间的复杂关系,同时保持整体视图的清晰。BOUNDED CONTEXT使我们能够在不同的部分中进行工作,而不会破坏模型或是无意间导致模型的分裂。把这些概念加进团队的UBIQUITOUS LANGUAGE中,可以帮助开发人员找出他们自己的解决方案。
保持模型的完整性
模型最基本的要求是它应该保持内部一致,术语总具有相同的意义,并且不包含互相矛盾的规则:虽然我们很少明确地考虑这些要求。模型的内部一致性又叫做统一(unification),这种情况下,每个术语都不会有模棱两可的意义,也不会有规则冲突。除非模型在逻辑上是一致的,否则它就没有意义。在理想世界中,我们可以得到涵盖整个企业领域的单一模型。这个模型将是统一的,没有任何相互矛盾或相互重叠的术语定义。每个有关领域的逻辑声明都是一致的。
但大型系统开发并非如此理想。**在整个企业系统中保持这种水平的统一是一件得不偿失的事情。**在系统的各个不同部分中开发多个模型是很有必要的,但我们必须慎重地选择系统的哪些部分可以分开,以及它们之间是什么关系。我们需要用一些方法来保持模型关键部分的高度统一。所有这些都不会自行发生,而且光有良好的意愿也是没用的。它只有通过有意识的设计决策和建立特定过程才能实现。大型系统领域模型的完全统一既不可行,也不划算。
有时人们会反对这一点。大多数人都看到了多个模型的代价:它们限制了集成,并且使沟通变得很麻烦。更重要的是,多个模型看上去似乎不够雅致。有时,对多个模型的抵触会导致"极富雄心"的尝试——将一个大型项目中的所有软件统一到单一模型中。但请一定要考虑下面的风险。
(1) 一次尝试对遗留系统做过多的替换。
(2) 大项目可能会陷入困境,因为协调的开销太大,超出了这些项目的能力范围。
(3) 具有特殊需求的应用程序可能不得不使用无法充分满足需求的模型,而只能将这些无法满足的行为放到其他地方。
(4) 另一方面,试图用一个模型来满足所有人的需求可能会导致模型中包含过于复杂的选择,因而很难使用。
此外,除了技术上的因素以外,权力上的划分和管理级别的不同也可能要求把模型分开。而且不同模型的出现也可能是团队组织和开发过程导致的结果。因此,即使完全的集成没有来自技术方面的阻力,项目也可能会面临多个模型。
**既然无法维护一个涵盖整个企业的统一模型,那就不要再受到这种思路的限制。通过预先决定什么应该统一,并实际认识到什么不能统一,我们就能够创建一个清晰的、共同的视图。**确定了这些之后,就可以着手开始工作,以保证那些需要统一的部分保持一致,不需要统一的部分不会引起混乱或破坏模型。
我们需要用一种方式来标记出不同模型之间的边界和关系。我们需要有意识地选择一种策略,并一致地遵守它。
本章将介绍一些用于识别、沟通和选择模型边界及关系的技术。讨论首先从描绘项目当前的范围开始。BOUNDED CONTEXT(限界上下文)定义了每个模型的应用范围,而CONTEXT MAP(上下文图)则给出了项目上下文以及它们之间关系的总体视图。这些降低模糊性的技术能够使项目更好地进行,但仅仅有它们还是不够的。一旦确立了CONTEXT的边界之后,仍需要持续集成这种过程,它能够使模型保持统一。
其后,在这个稳定的基础之上,我们就可以开始实施那些在界定和关联CONTEXT方面更有效的策略了——从通过共享内核(SHARED KERNEL)来紧密关联上下文,到那些各行其道(SEPARATE WAYS)地进行松散耦合的模型。
模式:BOUNDED CONTEXT
BOUNDED CONTEXT(限界上下文)——特定模型的限界应用。限界上下文使团队所有成员能够明确地知道什么必须保持一致,什么必须独立开发。
大型项目上会有多个模型共存,在很多情况下这没什么问题。不同的模型应用于不同的上下文中。例如,你可能必须将你的新软件与一个外部系统集成,而你的团队对这个外部系统没有控制权。在这种情况下,任何人都明白这个外部系统是一种完全不同的上下文,不适用他们正在开发的模型,但还有很多情况是比较含糊和混乱的。如两个团队为同一个新系统开发不同的功能,那么他们使用的是同一个模型吗?他们的意图是至少共享其所做的一部分工作,但却没有界限告诉他们共享了什么、没有共享什么。而且他们也没有一个过程来维持共享模型,或快速检测模型是否有分歧。他们只是在系统行为突然变得不可预测时才意识到他们之间产生了分歧。
即使在同一个团队中,也可能会出现多个模型。团队的沟通可能会不畅,导致对模型的理解产生难以捉摸的冲突。原先的代码往往反映的是早先的模型概念,而这些概念与当前模型有着微妙的差别。
每个人都知道两个系统的数据格式是不同的,因此需要进行数据转换,但这只是问题的表面。
问题的根本在于两个系统所使用的模型不同。当这种差异不是来自外部系统,而是发生在同一个系统中时,它将更难发现。然而,所有大型团队项目都会发生这种情况。任何大型项目都会存在多个模型。而当基于不同模型的代码被组合到一起后,软件就会出现bug、变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型不应该在哪个上下文中使用。
模型混乱的问题最终会在代码不能正常运行时暴露出来,但问题的根源却在于团队的组织方式和成员的交流方法。因此,为了澄清模型的上下文,我们既要注意项目,也要注意它的最终产品(代码、数据库模式等)。
**一个模型只在一个上下文中使用。这个上下文可以是代码的一个特定部分,也可以是某个特定团队的工作。**如果模型是在一次头脑风暴会议中得到的,那么这个模型的上下文可能仅限于那次讨论。模型上下文是为了保证该模型中的术语具有特定意义而必须要应用的一组条件。
为了解决多个模型的问题,我们需要明确地定义模型的范围——模型的范围是软件系统中一个有界的部分,这部分只应用一个模型,并尽可能使其保持统一。团队组织中必须一致遵守这个定义。
明确地定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。
BOUNDED CONTEXT明确地限定了模型的应用范围,以便让团队成员对什么应该保持一致以及上下文之间如何关联有一个明确和共同的理解。在CONTEXT中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。在其他CONTEXT中,会使用其他模型,这些模型具有不同的术语、概念、规则和UBIQUITOUS LANGUAGE的技术行话。通过划定明确的边界,可以使模型保持纯粹,因而在它所适用的CONTEXT中更有效。同时,也避免了将注意力切换到其他CONTEXT时引起的混淆。跨边界的集成必然需要进行一些转换,但我们可以清楚地分析这些转换。
BOUNDED CONTEXT不是MODULE
有时BOUNDED CONTEXT和MODULE易引起混淆,但它们是具有不同动机的不同模式。确实,当两组对象组成两个不同模型时,人们几乎总是把它们放在不同的MODULE中。这样做的确提供了不同的命名空间(对不同的CONTEXT很重要)和一些划分方法。
但人们也会在一个模型中用MODULE来组织元素,它们不一定要表达划分CONTEXT的意图。MODULE在BOUNDED CONTEXT内部创建的独立命名空间实际上使人们很难发现意外产生的模型分裂。
识别BOUNDED CONTEXT中的不一致
很多征兆都可能表明模型中出现了差异。最明显的是已编码的接口不匹配。对于更微妙的情况,一些意外行为也可能是一种信号。采用了自动测试的CONTINUOUS INTEGRATION可以帮助捕捉到这类问题。但语言上的混乱往往是一种早期的警告信号。
将不同模型的元素组合到一起可能会引发两类问题:重复的概念和假同源。重复的概念是指两个模型元素(以及伴随的实现)实际上表示同一个概念。每当这个概念的信息发生变化时,都必须更新两个地方。每次由于新知识导致一个对象被修改时,必须重新分析和修改另一个对象。如果不进行实际的重新分析,结果就会出现同一概念的两个版本,它们遵守不同的规则,甚至有不同的数据。更严重的是,团队成员必须学习做同一件事情的两种方法,以及保持这两种方法同步的各种方式。
假同源可能稍微少见一点,但它潜在的危害更大。**假同源是指使用相同术语(或已实现的对象)的两个人认为他们是在谈论同一件事情,但实际上并不是这样。**但是,当两个定义都与同一个领域方面相关,而只是在概念上稍有区别时,这种冲突更难以发现。假同源会导致开发团队互相干扰对方的代码,也可能导致数据库中含有奇怪的矛盾,还会引起团队沟通的混淆。假同源这个术语在自然语言中也经常使用。例如,说英语的人在学习西班牙语时常常会误用embarazada这个词。这个词的意思并不是embarrassed(难堪的),而是pregnant(怀孕的)。很惊讶吧!
当发现这些问题时,团队必须要做出相应的决定。可能需要将模型重新整合为一体,并加强用来预防模型分裂的过程。分裂也有可能是由分组造成的,一些小组出于合理的原因,需要以一些不同的方式来开发模型,而且你可能也决定让他们独立开发。本章接下来要讨论的模式的主题就是如何解决这些问题。
模式:CONTINUOUS INTEGRATION
定义完一个BOUNDED CONTEXT后,必须让它保持合理。
当很多人在同一个BOUNDED CONTEXT中工作时,模型很容易发生分裂。团队越大,问题就越大,但即使是3、4个人的团队也有可能会遇到严重的问题。然而,如果将系统分解为更小的CONTEXT,最终又难以保持集成度和一致性。
有时开发人员没有完全理解其他人所创建的对象或交互的意图,就对它进行了修改,使其失去了原来的作用。有时他们没有意识到他们正在开发的概念已经在模型的另一个部分中实现了,从而导致了这些概念和行为(不正确的)重复。有时他们意识到了这些概念有其他的表示,但却因为担心破坏现有功能而不敢去改动它们,于是他们继续重复开发这些概念和功能。
开发统一的系统(无论规模大小)需要维持很高的沟通水平,而这一点常常很难做到。我们需要运用各种方法来增进沟通并减小复杂性。还需要一些安全防护措施,以避免过于谨慎的行为(例如,开发人员由于担心破坏现有代码而重复开发一些功能)。
极限编程(XP)在这样的环境中真正显示出自己的强大威力。很多XP实践都是针对在很多人频繁更改设计的情况下如何维护设计的一致性这个特定问题而出现的。最纯粹的XP非常适合维护单一BOUNDED CONTEXT中的模型完整性。但是,无论是否使用XP,都很有必要采取CONTINUOUS INTEGRATION过程。
**CONTINUOUS INTEGRATION是指把一个BOUNDED CONTEXT中的所有工作足够频繁地合并到一起,并使它们保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。**像领域驱动设计中的其他方法一样,CONTINUOUS INTEGRATION也有两个级别的操作:(1) 模型的概念集成;(2) 实现的集成。
团队成员之间通过经常沟通来保证概念的集成。团队必须对不断变化的模型形成一个共同的理解。有很多方法可以帮助做到这一点,但最基本的方法是对UBIQUITOUS LANGUAGE多加锤炼。同时,实际工件通过系统性的合并/构建/测试过程来集成,这样能够尽早暴露出模型的分裂问题。用来集成的过程有很多,大部分有效的过程都具备以下这些特征:
分步集成,采用可重现的合并/构建技术。
自动测试套件。
有一些规则,用来为那些尚未集成的改动设置一个相当小的生命期上限。有效过程的另一面是概念集成,虽然它很少被正式地纳入进来。
在讨论模型和应用程序时要坚持使用UBIQUITOUS LANGUAGE。
大多数敏捷项目至少每天会把每位开发人员所做的修改合并进来。这个频率可以根据更改的步伐来调整,只要确保该间隔不会导致大量不兼容的工作产生即可。
在MODEL-DRIVEN DESIGN中,概念集成为实现集成铺平了道路,而实现集成验证了模型的有效性和一致性,并暴露出模型的分裂问题。
建立一个把所有代码和其他实现工件频繁地合并到一起的过程,并通过自动化测试来快速查明模型的分裂问题。严格坚持使用UBIQUITOUS LANGUAGE,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识。
最后,不要在持续集成中做一些不必要的工作。CONTINUOUS INTEGRATION只有在BOUNDED CONTEXT中才是重要的。相邻CONTEXT中的设计问题(包括转换)不必以同一个步调来处理。
CONTINUOUS INTEGRATION可以在任何单独的BOUNDED CONTEXT中使用,只要它的工作规模大到需要两个以上的人去完成就可以。它可以维护单一模型的完整性。当多个BOUNDED CONTEXT共存时,我们必须要确定它们的关系,并设计任何必需的接口。
模式:CONTEXT MAP
CONTEXT MAP(上下文图)——项目所涉及的限界上下文以及它们与模型之间的关系的一种表示。
只有一个BOUNDED CONTEXT并不能提供全局视图。其他模型的上下文可能仍不清楚而且还在不断变化。
其他团队中的人员并不是十分清楚CONTEXT的边界,他们会不知不觉地做出一些更改,从而使边界变得模糊或者使互连变得复杂。当不同的上下文必须互相连接时,它们可能会互相重叠。
BOUNDED CONTEXT之间的代码重用是很危险的,应该避免。功能和数据的集成必须要通过转换去实现。通过定义不同上下文之间的关系,并在项目中创建一个所有模型上下文的全局视图,可以减少混乱。
CONTEXT MAP位于项目管理和软件设计的重叠部分。按照常规,人们往往按团队组织的轮廓来划定边界。紧密协作的人会很自然地共享一个模型上下文。不同团队的人员(或者在同一个团队中但从不交流的人)将使用不同的上下文。办公室的物理位置也有影响,例如,分别位于大楼两端的团队成员(更不用说在不同城市工作的人了)如果没有为整合做额外的工作,很有可能会使用不同的上下文。大多数项目经理会本能地意识到这些因素,并围绕子系统大致把各个团队组织起来。但团队组织与软件模型及设计之间的相互关系仍然不够明显。对于软件模型与设计的持续概念细分,项目经理和团队成员需要一个清晰的视图。
识别在项目中起作用的每个模型,并定义其BOUNDED CONTEXT。这包括非面向对象子系统的隐含模型。为每个BOUNDED CONTEXT命名,并把名称添加到UBIQUITOUS LANGUAGE中。
描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。
先将当前的情况描绘出来。以后再做改变。
在每个BOUNDED CONTEXT中,都将有一种一致的UBIQUITOUS 0LANGUAGE的"方言"。我们需要把BOUNDED CONTEXT的名称添加到该方言中,这样只要通过明确CONTEXT就可以清楚地讨论任意设计部分的模型。
CONTEXT MAP无需拘泥于任何特定的文档格式。简图在可视化和沟通上下文图方面很有帮助。有些人可能喜欢使用较多的文本描述或别的图形表示。在某些情况下,团队成员之间的讨论就足够了。需求不同,细节层次也不同。不管CONTEXT MAP采用什么形式,它必须在所有项目人员之间共享,并被他们理解。它必须为每个BOUNDED CONTEXT提供一个明确的名称,而且必须阐明联系点和它们的本质。
根据设计问题和项目组织问题的不同,BOUNDED CONTEXT之间的关系有很多种形式。本章稍后将介绍CONTEXT之间的各种关系模式,这些模式分别适用于不同的情况,并且提供了一些术语,这些术语可以用来描述你在自己的上下文图中发现的关系。记住,CONTEXT MAP始终表示它当前所处的情况,你所发现的关系一开始可能并不适合这些模式。如果它们与某种模式非常接近,你可能想用这个模式名来描述它们,但不要生搬硬套。只需描述你所发现的关系即可。过后,你可以向更加标准化的关系过渡。
那么,如果你发现模型产生了分裂——模型完全混乱且包含不一致时,你该怎么办呢?这时一定要十分注意,先把描述工作停下来。然后,从精确的全局角度来解决这些混乱点。小的分裂可以修复,并且可以通过实施一些过程来为修复提供支持。如果关系很模糊,可以选择一种最接近的模式,然后向此模式靠拢。最重要的任务是画出一个清晰的CONTEXT MAP,而这可能意味着修复实际发现的问题。但不要因为修复必要的问题而重组整个结构。我们只需修改那些明显的矛盾即可,直到得出一个明确的CONTEXT MAP,在这个图中,你的所有工作都被放到某个BOUNDED CONTEXT中,而且所有互连的模型都有明确的关系。
一旦获得了一致的CONTEXT MAP,就会看到需要修改的那些地方。在经过深思熟虑后,你可以调整团队的组织或设计。记住,在更改实际上完成以前,不要先修改CONTEXT MAP。
模型上下文总是存在的,但如果我们不注意的话,它们可能会发生重叠和变化。通过明确地定义BOUNDED CONTEXT和CONTEXT MAP,团队就可以掌控模型的统一过程,并把不同的模型连接起来。
测试CONTEXT的边界
对各个BOUNDED CONTEXT的联系点的测试特别重要。这些测试有助于解决转换时所存在的一些细微问题以及弥补边界沟通上存在的不足。测试充当了有用的早期报警系统,特别是在我们必须信赖那些模型细节却又无法控制它们时,它能让我们感到放心。
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上下文的转换器。”
BOUNDED CONTEXT之间的关系
下面介绍的这些模式涵盖了将两个模型关联起来的众多策略。把模型连接到一起之后,就能够把整个企业笼括在内。这些模式有着双重目的,一是为成功地组织开发工作设定目标,二是为描述现有组织提供术语。
现有关系可能与这些模式中的某一种很接近——这可能是由于巧合,也可能是有意设计的——在这种情况下可以使用这个模式的术语来描述关系,但差异之处应该引起重视。然后,随着每次小的设计修改,关系会与所选定的模式越来越接近。
另一方面,你可能会发现现有关系很混乱或过于复杂。要想得到一个明确的CONTEXT MAP,需要重新组织一些关系。在这种情况或任何需要考虑重组的情况下,这些模式提供了应对各种不同情况的选择。这些模式的主要区别包括你对另一个模型的控制程度、两个团队之间合作水平和合作类型,以及特性和数据的集成程度。
下面这些模式涵盖了一些最常见和最重要的情况,它们提供了一些很好的思路,沿着这些思路,我们就可以知道如何处理其他情况。开发一个紧密集成产品的优秀团队可以部署一个大的、统一的模型。如果团队需要为不同的用户群提供服务,或者团队的协调能力有限,可能就需要采用SHARED KERNEL(共享内核)或CUSTOMER/SUPPLIER(客户/供应商)关系。有时仔细研究需求之后可能发现集成并不重要,而系统最好采用SEPARATE WAY(各行其道)模式。当然,大多数项目都需要与遗留系统或外部系统进行一定程度的集成,这就需要使用OPEN HOST SERVICE(开放主机服务)或ANTICORRUPTION LAYER(防腐层)。
模式:SHARED KERNEL
当功能集成受到局限,CONTINUOUS INTEGRATION的开销可能会变得非常高。尤其是当团队的技能水平或行政组织不能保持持续集成,或者只有一个庞大的、笨拙的团队时,更容易发生这种情况。在这种情况下就要定义单独的BOUNDED CONTEXT,并组织多个团队。
当不同团队开发一些紧密相关的应用程序时,如果团队之间不进行协调,即使短时间内能够取得快速进展,但他们开发出的产品可能无法结合到一起。最后可能不得不耗费大量精力在转换层上,并且频繁地进行改动,不如一开始就使用CONTINUOUS INTEGRATION那么省心省力,同时这也造成重复工作,并且无法实现公共的UBIQUITOUS LANGUAGE所带来的好处。
在很多项目中,一些基本上独立工作的团队共享基础设施层。领域工作采用类似的方法也可以得到很好的效果。保持整个模型和代码完全同步的开销可能太高了,但从系统中仔细挑选出一部分并保持同步,就能以较小的代价获得较大的收益。
从领域模型中选出两个团队都同意共享的一个子集。当然,除了这个模型子集以外,还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位,一个团队在没与另一个团队商量的情况下不应擅自更改它。功能系统要经常进行集成,但集成的频率应该比团队中CONTINUOUS INTEGRATION的频率低一些。在进行这些集成的时候,两个团队都要运行测试。
这是一个仔细的平衡。SHARED KERNEL(共享内核)不能像其他设计部分那样自由更改。在做决定时需要与另一个团队协商。**共享内核中必须集成自动测试套件,因为修改共享内核时,必须要通过两个团队的所有测试。**通常,团队先修改各自的共享内核副本,然后每隔一段时间与另一个团队的修改进行集成。例如,在每天(或更短的时间周期)进行CONTINUOUS INTEGRATION的团队中,可以每周进行一次内核的合并。不管代码集成是怎样安排的,两个团队越早讨论修改,效果就会越好。
**SHARED KERNEL通常是CORE DOMAIN(核心领域),或是一组GENERIC SUBDOMAIN(通用子领域),也可能二者兼有,它可以是两个团队都需要的任何一部分模型。**使用SHARED KERNEL的目的是减少重复(并不是消除重复,因为只有在一个BOUNDED CONTEXT中才能消除重复),并使两个子系统之间的集成变得相对容易一些。
模式:CUSTOMER/SUPPLIER DEVELOPMENT TEAM
我们常常会碰到这样的情况:一个子系统主要服务于另一个子系统;"下游"组件执行分析或其他功能,这些功能向"上游"组件反馈的信息非常少,所有依赖都是单向的。两个子系统通常服务于完全不同的用户群,其执行的任务也不同,在这种情况下使用不同的模型会很有帮助。工具集可能也不相同,因此无法共享程序代码。
上游和下游子系统很自然地分隔到两个BOUNDED CONTEXT中。如果两个组件需要不同的技能或者不同的工具集来实现时,更需要把它们隔离到不同的上下文中。转换很容易,因为只需要进行单向转换。但两个团队的行政组织关系可能会引起问题。
如果下游团队对变更具有否决权,或请求变更的程序太复杂,那么上游团队的开发自由度就会受到限制。由于担心破坏下游系统,上游团队甚至会受到抑制。同时,由于上游团队掌握优先权,下游团队有时也会无能为力。
下游团队依赖于上游团队,但上游团队却不负责下游团队的产品交付。要琢磨拿什么来影响对方团队,是人性呢,还是时间压力,亦或其他诸如此类的,这需要耗费大量额外的精力。因此,正式规定团队之间的关系会使所有人工作起来更容易。这样,就可以对开发过程进行组织,均衡地处理两个用户群的需求,并根据下游所需的特性来安排工作。
在极限编程项目中,已经有了实现此目的的机制——迭代计划过程。我们只需根据计划过程来定义两个团队之间的关系。下游团队的代表类似于用户代表,参加上游团队的计划会议,上游团队直接与他们的"客户"同仁讨论和权衡其所需的任务。结果是供应商团队得到一个包含下游团队最需要的任务的迭代计划,或是通过双方商定推迟一些任务,这样下游团队也就知道这些被推迟的功能不会交付给他们。(在上游团队敲定需求的时候,所有的下游团队参与需求评审,当需求评审通过后,通过迭代计划来满足不同下游团队的各自需求)
如果使用的不是XP过程,那么无论使用什么类似的方法来平衡不同用户的关注点,都可以对这种方法加以扩充,使之把下游应用程序的需求包括进来。
在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。
两个团队共同开发自动化验收测试,用来验证预期的接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对下游团队产生副作用。–理念和契约测试类似
在迭代期间,下游团队成员应该像传统的客户一样随时回答上游团队的提问,并帮助解决问题。
自动化验收测试是这种客户关系的一个重要部分。即使在合作得非常好的项目中,虽然客户很明确他们所依赖的功能并告诉上游团队,而且供应商也能很认真地把所做的修改传递给下游团队,但如果没有测试,仍然会发生一些很意外的事情。这些事情将破坏下游团队的工作,并使上游团队不得不采取计划外的紧急修复措施。因此,客户团队在与供应商团队合作的过程中,应该开发自动验收测试来验证所期望的接口。上游团队将把这些测试作为标准测试套件的一部分来运行。任何一个团队在修改这些测试时都需要与另一个团队沟通,因为修改测试就意味着修改接口。
当某个客户对供应商的业务至关重要时,不同公司的项目之间也会出现客户/供应商关系。**下游团队也能制约上游团队,一个有影响力的客户所提出的要求对上游项目的成功非常重要,但这些要求也能破坏上游项目的开发。**建立正式的需求响应过程对双方都有利,因为与内部IT关系相比,在这种外部关系中更难做出"成本/效益"的权衡。
这种模式有两个关键要素。
(1) 关系必须是客户与供应商的关系,其中客户的需求是至关重要的。由于下游团队并不是唯一的客户,因此不同客户的要求必须通过协商来平衡,但这些要求都是非常重要的。这种关系与那种经常出现的"穷亲威"关系相反,在后者的关系中,下游团队不得不乞求上游团队满足其需求。
(2) 必须有自动测试套件,使上游团队在修改代码时不必担心破坏下游团队的工作,并使下游团队能够专注于自己的工作,而不用总是密切关注上游团队的行动。
在接力赛中,前面的选手在接棒的时候不能一直回头看,这位选手必须相信队友能够把接力棒准确地交到他手中,否则整个团队的速度无疑会慢下来。
CUSTOMER/SUPPLIER TEAM涉及的团队如果能在同一个部门中工作,最后会形成共同的目标,这样成功机会将更大一些,如果两个团队分属不同的公司,但实际上也具有这些角色,同样也容易成功。但是,当上游团队不愿意为下游团队提供服务时,情况就会完全不同!!!
模式:CONFORMIST
当两个具有上游/下游关系的团队不归同一个管理者指挥时,CUSTOMER/SUPPLIER TEAM这样的合作模式就不会奏效。勉强应用这种模式会给下游团队带来麻烦。大公司可能会发生这种情况,其中两个团队在管理层次中相隔很远,或者两个团队的共同主管不关心它们之间的关系。当两个团队属于不同公司时,如果客户的业务对供应商不是非常重要,那么也会出现这种情况。或许供应商有很多小客户,或者供应商正在改变市场方向,而不再重视老客户。也可能是供应商的运营状况较差,或者已经倒闭。不管是什么原因,现实情况是下游团队只能靠自己了。
当两个开发团队具有上/下游关系时,如果上游团队没有动力来满足下游团队的需求,那么下游团队将无能为力。出于利他主义的考虑,上游开发人员可能会做出承诺,但他们可能不会履行承诺。下游团队出于良好的意愿会相信这些承诺,从而根据一些永远不会实现的特性来制定计划。下游项目只能被搁置,直到团队最终学会利用现有条件自力更生为止。下游团队不会得到根据他们的需求而量身定做的接口。
在这种情况下,有3种可能的解决途径。一种是完全放弃对上游的使用。做出这种选择时,应进行切实地评估,绝不要假定上游会满足下游的需求。有时我们会高估这种依赖性的价值,或是低估它的成本。如果下游团队决定切断这条链,他们将走上SEPARATE WAY(各行其道)的道路(参见本章后面介绍的模式)。
有时,使用上游软件具有非常大的价值,因此必须保持这种依赖性(或者是行政决策规定团队不能改变这种依赖性)。在这种情况下,还有两种途径可供选择,选择哪一种取决于上游设计的质量和风格。如果上游的设计很难使用(可能是由于缺乏封装、使用了不恰当的抽象或者建模时使用了下游团队无法使用的范式),那么下游团队仍然需要开发自己的模型。他们将担负起开发转换层的全部责任,这个层可能会非常复杂(参见本章后面要介绍的ANTICORRUPTION LAYER)。另一方面,如果上游设计的质量不是很差,而且风格也能兼容的话,那么最好不要再开发一个独立的模型。这种情况下可以使用CONFORMIST(跟随者)模式。
通过严格遵从上游团队的模型,可以消除在BOUNDED CONTEXT之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择CONFORMITY模式可以极大地简化集成。此外,这样还可以与供应商团队共享UBIQUITOUS LANGUAGE。供应商处于统治地位,因此最好使沟通变容易。他们从利他主义的角度出发,会与你分享信息。
选用CONFORMIST模式的决策,会加深你对上游团队的依赖。同时你的应用也受限于上游模型的功能,充其量也只能做一些简单的增强而已。人们在主观上不愿意这样做,因此有时本应该这样选择时,却没有这样选择。如果这些折中不可接受,而上游的依赖又必不可少,那么还可以选择第二种方法。通过创建一个ANTICORRUPTION LAYER来尽可能把自己隔离开,这是一种实现转换映射的积极方法,后面将会讨论它。
CONFORMIST模式类似于SHARED KERNEL模式。在这两种模式中,都有一个重叠的区域——在这个重叠区域内模型是相同的,此外还有你的模型所扩展的部分,以及另一个模型对你没有影响的部分。这两种模式之间的区别在于决策制定和开发过程不同。SHARED KERNEL是两个高度协调的团队之间的合作模式,而CONFORMIST模式则是应对与一个对合作不感兴趣的团队进行集成。
前面介绍了在两个BOUNDED CONTEXT之间集成时可以进行的各种合作,从高度合作的SHARED KERNEL模式或CUSTOMER/SUPPLIER DEVELOPER TEAM到单方面的CONFORMIST模式。现在,我们最后来看一种更悲观的关系,假设另一个团队既不合作,而且其设计也无法使用时,该如何应对。
模式:ANTICORRUPTION LAYER
新系统几乎总是需要与遗留系统或其他系统进行集成,这些系统具有其自己的模型。当把参与集成的BOUNDED CONTEXT设计完善并且团队相互合作时,转换层可能很简单,甚至很优雅。但是,当边界那侧发生渗透时,转换层就要承担起更多的防护职责。
当正在构建的新系统与另一个系统的接口很大时,为了克服连接两个模型而带来的困难,新模型所表达的意图可能会被完全改变,最终导致它被修改得像是另一个系统的模型了(以一种特定的风格)。遗留系统的模型通常很弱。即使对于那些模型开发得很好的例外情况,它们可能也不符合当前项目的需要。然而,集成遗留系统仍然具有很大的价值,而且有时还是绝对必要的。
正确答案是不要全盘封杀与其他系统的集成。人们非常热衷于替换所有遗留系统,但由于工作量太大,这不可能立即完成。此外,与现有系统集成是一种有价值的重用形式。在大型项目中,一个子系统通常必须与其他独立开发的子系统连接。这些子系统将从不同角度反映问题领域。当基于不同模型的系统被组合到一起时,为了使新系统符合另一个系统的语义,新系统自己的模型可能会被破坏。即使另一个系统被设计得很好,它也不会与客户基于同一个模型。而且其他系统往往并不是设计得很好。
当通过接口与外部系统连接时,存在很多障碍。例如,基础设施层必须提供与另一个系统进行通信的方法,那个系统可能处于不同的平台上,或是使用了不同的协议。你必须把那个系统数据类型转换为你自己系统的数据类型。但通常被忽视的一个事实是那个系统肯定不会使用相同的概念领域模型。
如果从一个系统中取出一些数据,然后在另一个系统中错误地解释了它,那么显然会发生错误,甚至会破坏数据库。尽管我们已经认识到这一点,这个问题仍然会"偷袭"我们,因为我们认为在系统之间转移的是原始数据,其含义是明确的,并且认为这些数据在两个系统中的含义肯定是相同的。这种假设常常是错误的。数据与每个系统的关联方式会使数据的含义出现细微但重要的差别。而且,即使原始数据元素确实具有完全相同的含义,但在原始数据这样低的层次上进行接口操作通常是错误的。这样的底层接口使另一个系统的模型丧失了解释数据以及约束其值和关系的能力,同时使新系统背负了解释原始数据的负担(而且并未使用这些数据自己的模型)。
我们需要在不同模型的关联部分之间建立转换机制,这样模型就不会被未经消化的外来模型元素所破坏。
创建一个防腐层,以便根据客户自己的领域模型来为客户提供相关功能。这个层通过另一个系统现有接口与其进行对话,而只需对那个系统作出很少的修改,甚至无需修改。在内部,防腐层在两个模型之间进行必要的双向转换。
这种连接两个系统的机制可能会使我们想到把数据从一个程序传输到另一个程序,或者从一个服务器传输到另一个服务器。我们很快就会讨论技术通信机制的使用。但这些细节问题不应与ANTICORRUPTION LAYER混淆,因为ANTICORRUPTION LAYER并不是向另一个系统发送消息的机制。相反,它是在不同的模型和协议之间转换概念对象和操作的机制。
ANTICORRUPTION LAYER本身就可能是一个复杂的软件。接下来将概要描述在创建ANTICORRUPTION LAYER时需要考虑的一些事项。
设计ANTICORRUPTION LAYER的接口
ANTICORRUPTION LAYER的公共接口通常以一组SERVICE的形式出现,但偶尔也会采用ENTITY的形式。构建一个全新的层来负责两个系统之间的语义转换为我们提供了一个机会,它使我们能够重新对另一个系统的行为进行抽象,并按照与我们的模型一致的方式把服务和信息提供给我们的系统。在我们的模型中,把外部系统表示为一个单独的组件可能是没有意义的。最好是使用多个SERVICE(或偶尔使用ENTITY),其中每个SERVICE都使用我们的模型来履行一致的职责。
实现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的基本元素。此外,还有其他一些需要考虑的因素。
一般情况下,都是由正在设计的系统(你的子系统)向其他子系统发起一个动作。但在有些情况下,其他子系统可能需要向你的子系统提交某种请求,或是把某个事件通知给你的子系统。ANTICORRUPTION LAYER可以是双向的,它可能使用具有对称转换的相同转换器来定义两个接口上的SERVICE(并使用各自的ADAPTER)。尽管实现ANTICORRUPTION LAYER通常不需要对另一个子系统做任何修改,但为了使它能够调用ANTICORRUPTION LAYER的SERVICE,有时还是有必要修改的。防腐层结构如下图所示:
我们通常需要一些通信机制来连接两个子系统,而且它们可能位于不同的服务器上。在这种情况下,必须决定在哪里放置通信链接。如果无法访问另一个子系统,那么可能必须在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来减少它们之间的矛盾。隔离策略的益处必须平衡它产生的代价。我们应该从实际出发,对模型做出适度的修改,使之能够更好地适应外部模型。
任何集成都是有开销的,无论这种集成是单一BOUNDED CONTEXT中的完全CONTINUOUS INTEGRATION,还是集成度较轻的SHARED KERNEL或CUSTOMER/SUPPLIER DEVELOPER TEAM,或是单方面的CONFORMIST模式和防御型的ANTICORRUPTION LAYER模式。集成可能非常有价值,但它的代价也总是十分高昂的。我们应该确保在真正需要的地方进行集成。
模式:SEPARATE WAY
我们必须严格划定需求的范围。如果两组功能之间的关系并非必不可少,那么二者完全可以彼此独立。集成总是代价高昂,而有时获益却很小。
除了在团队之间进行协调所需的常见开销以外,集成还迫使我们做出一些折中。可以满足某一特定需求的简单专用模型要为能够处理所有情况的更加抽象的模型让路。或许有些完全不同的技术能够轻而易举地提供某些特性,但它却难以集成。或许某个团队很难合作,使得其他团队在尝试与之合作时找不到行之有效的方法。
在很多情况下,集成不会提供明显的收益。如果两个功能部分并不需要互相调用对方的功能,或者这两个部分所使用的对象并不需要进行交互,或者在它们操作期间不共享数据,那么集成可能就是没有必要的(尽管可以通过一个转换层进行集成)。仅仅因为特性在用例中相关,并不一定意味着它们必须集成到一起。
声明一个与其他上下文毫无关联的BOUNDED CONTEXT,使开发人员能够在这个小范围内找到简单、专用的解决方案。
特性仍然可以被组织到中间件或UI层中,但它们将没有共享的逻辑,而且应该把通过转换层进行的数据传输减至最小,最好是没有数据传输。
采用SEPARATE WAY(各行其道)模式需要预先决定一些选项。尽管持续重构最后可以撤销任何决策,但完全隔离开发的模型是很难合并的。如果最终仍然需要集成,那么转换层将是必要的,而且可能很复杂。当然,不管怎样,这都是我们将要面对的问题。
模式:OPEN HOST SERVICE
一般来说,在BOUNDED CONTEXT中工作时,我们会为CONTEXT外部的每个需要集成的组件定义一个转换层。当集成是一次性的,这种为每个外部系统插入转换层的方法可以以最小的代价避免破坏模型。但当子系统要与很多系统集成时,可能就需要更灵活的方法了。
当一个子系统必须与大量其他系统进行集成时,为每个集成都定制一个转换层可能会减慢团队的工作速度。需要维护的东西会越来越多,而且进行修改的时候担心的事情也会越来越多。
团队可能正在反复做着同样的事情。如果一个子系统有某种内聚性,那么或许可以把它描述为一组SERVICE,这组SERVICE满足了其他子系统的公共需求。
要想设计出一个足够干净的协议,使之能够被多个团队理解和使用,是一件十分困难的事情,因此只有当子系统的资源可以被描述为一组内聚的SERVICE并且必须进行很多集成的时候,才值得这样做。在这些情况下,它能够把维护模式和持续开发区别开。
定义一个协议,把你的子系统作为一组SERVICE供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。
这种通信形式暗含一些共享的模型词汇,它们是SERVICE接口的基础。这样,其他子系统就变成了与OPEN HOST(开放主机)的模型相连接,而其他团队则必须学习HOST团队所使用的专用术语。在一些情况下,使用一个众所周知的PUBLISHED LANGUAGE(公开发布的语言)作为交换模型可以减少耦合并简化理解。
最简单的形式,就是公开后端API或公开业务功能。
模式:PUBLISHED LANGUAGE
两个BOUNDED CONTEXT之间的模型转换需要一种公共的语言。
当两个领域模型必须共存而且必须交换信息时,转换过程本身就可能很复杂,而且很难文档化和理解。如果正在构建一个新系统,我们一般会认为新模型是最好的,因此只考虑把其他模型转换成新模型就可以了。但有时我们的工作是增强一系列旧系统并尝试集成它们。这时要在众多模型中选择一个比较不烂的模型,也就是说"两害取其轻"。
另一种情况是,当不同业务之间需要互相交换信息时,应该如何做?想让一个业务采用另一个业务的领域模型不仅是不现实的,而且可能也不符合双方的需要。领域模型是为了解决其用户的需求而开发的,这样的模型所包含的一些特性可能使得与另一个系统的通信变得复杂,而实际上没有必要这么复杂。此外,如果把一个应用程序的模型用作通信媒介,那么它可能就无法为满足新需求而自由地修改了,它必须非常稳定,以便支持当前的通信职责。
与现有领域模型进行直接的转换可能不是一种好的解决方案。这些模型可能过于复杂或设计得较差。它们可能没有被很好地文档化。如果把其中一个模型作为数据交换语言,它实质上就被固定住了,而无法满足新的开发需求。
OPEN HOST SERVICE使用一个标准化的协议来支持多方集成。它使用一个领域模型来在各系统间进行交换,尽管这些系统的内部可能并不使用该模型。这里我们可以更进一步——发布这种语言,或找到一种已经公开发布的语言。我这里所说的发布仅仅是指该语言已经可以供那些对它感兴趣的群体使用,而且已经被充分文档化,兼容一些独立的解释。
如XML的一个非常有价值的特性是通过DTD(文档类型定义)或XML模式来正式定义一个专用的领域语言,从而使得数据可以被转换为这种语言。一些行业组织已经成立,准备为各自的行业定义一种标准的DTD,这样,业内多方就可以交换信息了,如交换化学公式信息或遗传代码信息。实际上这些组织正在以语言定义的形式创建一种共享的领域模型。
把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。
"大象"的统一
在盲人摸象的故事中,大象模型的统一要比大多数这样的合并相对简单一些。遗憾的是,大象模型的统一只是一个特例——不同模型纯粹是在描述整体的不同部分,然而,这通常是模型之间差别的一个方面而已。当两个模型以不同方式描述同一部分时,问题会变得更加困难。如果两个盲人都摸到了象鼻子,一个人认为它像蛇,而另一个人认为它像消防水管,那么他们将更难集成。双方都无法接受对方的模型,因为那不符合自己的体验。事实上,他们需要一个新的抽象,这个抽象需要把蛇的"活着的特性"与消防水管的喷水功能合并到一起,而这个抽象还应该排除先前两个模型中的一些不确切的含义,如人们可能会想到的毒牙,或者可以从身体上拆下并卷起来放到救火车中的这种性质。
尽管我们已经把部分合并成一个整体,但得到的模型还是很简陋的。它缺乏内聚性,也没有形成任何潜在领域的轮廓。在持续精化的过程中,新的理解可能会产生更深层的模型。新的应用程序需求也可能会促成更深层的模型。如果大象开始移动了,那么"树"理论就站不住脚了,而盲人建模者们也可能会有所突破,形成"腿"的概念。
模型集成的第二步是去掉各个模型中那些偶然或不正确的方面,并创建新的概念,在本例中,这个概念就是一种"动物",它长着"鼻子"、“腿”、“身体"和"尾巴”,每个部分都有其自己的属性以及与其他部分的明确关系。在很大程度上,成功的模型应该尽可能做到精简。象鼻与蛇相比,其特性和功能可能比蛇多,也可能比蛇少,但宁"少"勿"多"。宁可缺少喷水功能,也不要包含不正确的毒牙特性。
如果目标只是找到大象,那么只要对每个模型中所表示的位置进行转换就可以了。当需要更多集成时,第一个版本的统一模型不一定达到完全的成熟。把大象看成一堵墙,下面用树干支撑着,一头儿是一根绳子,另一头儿是一条蛇,就可以适当地满足一些需求了。紧接着,通过新需求和进一步的理解及沟通的推动,模型可以得到加深和精化。
承认多个互相冲突的领域模型实际上正是面对现实的做法。通过明确定义每个模型都适用的上下文,可以维护每个模型的完整性,并清楚地看到要在两个模型之间创建的任何特殊接口的含义。盲人没办法看到整个大象,但只要他们承认各自的理解是不完整的,他们的问题就能得到解决。
选择模型的限界上下文的策略
在任何时候,绘制出CONTEXT MAP来反映当前状况都是很重要的。但是,一旦绘制好CONTEXT MAP之后,你很可能想要改变现状。现在,你可以开始有意识地选择CONTEXT的边界和关系。以下是一些指导原则。
团队决策或更高层决策
首先,团队必须决定在哪里定义BOUNDED CONTEXT,以及它们之间有什么样的关系。这些决策必须由团队做出,或者至少传达给整个团队,并且被团队里的每个人理解。事实上,这样的决策通常需要与外部团队达成一致。按照本身价值来说,在决定是否扩展或分割BOUNDED CONTEXT时,应该权衡团队独立工作的价值以及能产生直接且丰富集成的价值,以这两种价值的成本——效益作为决策的依据。在实践中,团队之间的行政关系往往决定了系统的集成方式。由于汇报结构,有技术优势的统一可能无法实现。管理层所要求的合并可能并不实用。你不会总能得到你想要的东西,但你至少可以评估出这些决策的代价,并反映给管理层,以便采取相应的措施来减小代价。从一个现实的CONTEXT MAP开始,并根据实际情况来选择改变。
置身上下文中
开发软件项目时,我们首先是对自己团队正在开发的那些部分感兴趣(“设计中的系统”),其次是对那些与我们交互的系统感兴趣。典型情况下,设计中的系统将被划分为一到两个BOUNDED CONTEXT,开发团队的主力将在这些上下文中工作,或许还会有另外一到两个起支持作用的CONTEXT。除此之外,就是这些CONTEXT与外部系统之间的关系。这是一种简单、典型的情况,能让你对可能会遇到的情形有一些粗略的了解。
实际上,我们正是自己所处理的主要CONTEXT的一部分,这会在我们的CONTEXT MAP中反映出来。只要我们知道自己存在偏好,并且在超出该CONTEXT MAP的应用边界时能够意识到已越界,那么就不会有什么问题。
转换边界
在画出BOUNDED CONTEXT的边界时,有无数种情况,也有无数种选择。但权衡时所要考虑的通常是下面所列出的某些因素。
首选较大的BOUNDED CONTEXT
当用一个统一模型来处理更多任务时,用户任务之间的流动更顺畅。
一个内聚模型比两个不同模型再加它们之间的映射更容易理解。
两个模型之间的转换可能会很难(有时甚至是不可能的)。
共享语言可以使团队沟通起来更清楚。
首选较小的BOUNDED CONTEXT
开发人员之间的沟通开销减少了。
由于团队和代码规模较小,CONTINUOUS INTEGRATION更容易了。
较大的上下文要求更加通用的抽象模型,而掌握所需技巧的人员会出现短缺。
不同的模型可以满足一些特殊需求,或者是能够把一些特殊用户群的专门术语和UBIQUITOUS LANGUAGE的专门术语包括进来。
在不同BOUNDED CONTEXT之间进行深度功能集成是不切实际的。在一个模型中,只有那些能够严格按照另一个模型来表述的部分才能够进行集成,而且,即便是这种级别的集成可能也需要付出相当大的工作量。当两个系统之间有一个很小的接口时,集成是有意义的。
接受那些我们无法更改的事物:描述外部系统
最好从一些最简单的决策开始。一些子系统显然不在开发中的系统的任何BOUNDED CONTEXT中。一些无法立即淘汰的大型遗留系统和那些提供所需服务的外部系统就是这样的例子。我们很容易就能识别出这些系统,并把它们与你的设计隔离开。
在做出假设时必须要保持谨慎。我们会很轻易地认为这些系统构成了其自己的BOUNDED CONTEXT,但大多数外部系统只是勉强满足定义。首先,定义BOUNDED CONTEXT的目的是把模型统一在特定边界之内。你可能负责遗留系统的维护,在这种情况下,可以明确地声明这一目的,或者也可以很好地协调遗留团队来执行非正式的CONTINUOUS INTEGRATION,但不要认为遗留团队的配合是理所当然的事情。仔细检查,如果开发工作集成得不好,一定要特别小心。在这样的系统中,不同部分之间出现语义矛盾是很平常的事情。
与外部系统的关系
这里可以应用3种模式。首先,可以考虑SEPARATE WAY模式。当然,如果你不需要集成,就不用把它们包括进来。但一定要真正确定不需要集成。只为用户提供对两个系统的简单访问确实够用吗?集成要花费很大代价而且还会分散精力,因此要尽可能为你的项目减轻负担。
如果集成确实非常重要,可以在两种极端的模式之中进行选择:CONFORMIST模式或ANTICORRUPTION LAYER模式。作为CONFORMIST并不那么有趣,你的创造力和你对新功能的选择都会受到限制。当构建一个大型的新系统时,遵循遗留系统或外部系统的模型可能是不现实的(毕竟,为什么要构建新系统呢?)。但是,当对一个大的系统进行外围扩展时,而且这个系统仍然是主要系统,在这种情况下,继续使用遗留模型可能就很合适。这种选择的例子包括轻量级的决策支持工具,这些工具通常是用Excel或其他简单工具编写的。如果你的应用程序确实是现有系统的一个扩展,而且与该系统的接口很大,那么CONTEXT之间转换所需的工作量可能比应用程序功能本身需要的工作量还大。尽管你已经处于另一个系统的BOUNDED CONTEXT中,但你自己的一些好的设计仍然有用武之地。如果另一个系统有着可以识别的领域模型,那么只要使这个模型比在原来的系统中更清晰,你就可以改进你的实现,唯一需要注意的是要严格地遵照那个老模型。如果你决定采用CONFORMIST设计,就必须全心全意地去做。你应该约束自己只可以去扩展现有模型,而不能去修改它。
当正在设计的系统功能并不仅仅是扩展现有系统时,而且你与另一个系统的接口很小,或者另一个系统的设计非常糟糕,那么实际上你会希望使用自己的BOUNDED CONTEXT,这意味着需要构建一个转换层,甚至是一个ANTICORRUPTION LAYER。
设计中的系统
你的项目团队正在构建的软件就是设计中的系统。你可以在这个区域内声明BOUNDED CONTEXT,并在每个BOUNDED CONTEXT中应用CONTINUOUS INTEGRATION,以便保持它们的统一。但应该有几个上下文呢?各个上下文之间又应该是什么关系呢?与外部系统的情况相比,这些问题的答案会变得更加不确定,因为我们拥有更多的主动权。
情况可能非常简单:设计中的整个系统使用一个BOUNDED CONTEXT。例如,当一个少于10人的团队正在开发高度相关的功能时,这可能就是一种很好的选择。
随着团队规模的增大,CONTINUOUS INTEGRATION可能会变得困难起来(尽管我也曾看到过一些较大的团队仍能保持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,但多个团队在一个上下文中工作却是比较难的(虽然并非不可能)。
用不同模型满足特殊需要
同一业务的不同小组常常有各自的专用术语,而且可能各不相同。这些本地术语可能是非常精确的,并且是根据他们的需要定制的。要想改变它们(例如,施行标准化的企业级术语),需要大量的培训和分析,以便解决差异问题。即使如此,新术语仍然可能没有原来那个已经经过精心调整的术语好用。
你可能决定通过不同的BOUNDED CONTEXT来满足这些特殊需要,除了转换层的CONTINUOUS INTEGRATION以外,让模型采用SEPARATE WAY模式。UBIQUITOUS LANGUAGE的不同专用术语将围绕这些模型以及它们所基于的行话来发展。如果两种专用术语有很多重叠之处,那么SHARED KERNEL模式就可以满足特殊化要求,同时又能把转换成本减至最小。
当不需要集成或者集成相对有限时,就可以继续使用已经习惯的术语,以免破坏模型。但这也有其自己的代价和风险。如下所示。
没有共同的语言,交流将会减少。
集成开销更高。
随着相同业务活动和实体的不同模型的发展,工作会有一定的重复。
但是,最大的风险或许是,它会成为拒绝改变的理由,或为古怪、狭隘的模型辩护。为了满足特殊的需要,需要对系统的这一部分进行多大的定制?最重要的是,这个用户群的专门术语有多大的价值?你必须在团队独立操作的价值与转换的风险之间做出权衡,并且留心合理地处理一些没有价值的术语变化。
有时会出现一个深层次的模型,它把这些不同语言统一起来,并能够满足双方的要求。只有经过大量开发工作和知识消化之后,深层次模型才会在生命周期的后期出现。深层次模型不是计划出来的,我们只能在它出现的时候抓住机遇,修改自己的策略并进行重构。
记住,在需要大量集成的地方,转换成本会大大增加。在团队之间进行一些协调工作(从精确地修改一个具有复杂转换的对象到采用SHARED KERNEL模式)可以使转换变得更加容易,同时又不需要完全的统一。
部署
在复杂系统中,对打包和部署进行协调是一项繁琐的任务,这类任务总是要比看上去难得多。**BOUNDED CONTEXT策略的选择将影响部署。**例如,当CUSTOMER/SUPPLIER TEAM部署新版本时,他们必须相互协调来发布经过共同测试的版本。在这些版本中,必须要进行代码和数据迁移。在分布式系统中,一种好的做法是把CONTEXT之间的所有转换层放在同一个进程中,这样就不会出现多个版本共存的情况。
当数据迁移可能很花时间或者分布式系统无法同步更新时,即使是单一BOUNDED CONTEXT中的组件部署也是很困难的,这会导致代码和数据有两个版本共存。
由于部署环境和技术存在不同,有很多技术因素需要考虑。但BOUNDED CONTEXT关系可以为我们指出重点问题。转换接口已经被标出。
绘制CONTEXT边界时应该反映出部署计划的可行性。当两个CONTEXT通过一个转换层连接时,要想更新其中的一个CONTEXT,新的转换层需要为另一个CONTEXT提供相同的接口。SHARED KERNEL需要进行更多的协调工作,不仅在开发中如此,而且在部署中也同样应该如此。SEPARATE WAY模式可以使工作简单很多。
权衡
通过总结这些指导原则可知有很多统一或集成模型的策略。一般来说,我们需要在无缝功能集成的益处和额外的协调和沟通工作之间做出权衡。还要在更独立的操作与更顺畅的沟通之间做出权衡。更积极的统一需要对有关子系统的设计有更多控制。
当项目正在进行时
很多情况下,我们不是从头开发一个项目,而是会改进一个正在开发的项目。在这种情况下,第一步是根据当前的状况来定义BOUNDED CONTEXT。这很关键。为了有效地定义上下文,CONTEXT MAP必须反映出团队的实际工作,而不是反映那个通过遵守以上描述的指导原则而得出的理想组织。
描述了当前真实的BOUNDED CONTEXT以及它们的关系以后,下一步就是围绕当前组织结构来加强团队的工作。在CONTEXT中加强CONTINUOUS INTEGRATION。把所有分散的转换代码重构到ANTICORRUPTION LAYER中。命名现有的BOUNDED CONTEXT,并确保它们处于项目的UBIQUITOUS LANGUAGE中。现在可以开始考虑修改边界和它们的关系了。这些修改很自然地由相同的原则来驱动——之前已经描述了在新项目上使用这些原则,但我们应该把这些修改分成较小的部分,以便根据实际情况做出选择,从而在只花费最少的工作和对模型产生最小破坏的前提下创造最大的价值。
BOUNDED CONTEXT的转换
像建模和设计的其他方面一样,有关BOUNDED CONTEXT的决策并非不可改变的。在很多情况下,我们必须改变最初有关边界以及BOUNDED CONTEXT之间关系的决策,这是不可避免的。一般而言,分割CONTEXT是很容易的,但合并它们或改变它们之间的关系却很难。下面将介绍几种有代表性的修改,它们很难,但也很重要。这些转换往往很大,无法在一次重构中完成,甚至无法在一次项目迭代中完成。因为这个原因,本文将把这些转换划分为一系列简单的步骤。当然,这些只是一些指导原则,你必须根据你的特殊情况和事件对它们进行调整。
合并CONTEXT:SEPARATE WAY→SHARED KERNEL
合并BOUNDED CONTEXT的动机很多:翻译开销过高、重复现象很明显。合并很难,但什么时候做都不晚,只是需要一些耐心。
即使你的最终目标是完全合并成一个采用CONTINUOUS INTEGRATION的CONTEXT,也应该先过渡到SHARED KERNEL。
(1) 评估初始状况。在开始统一两个CONTEXT之前,一定要确信它们确实需要统一。
(2) 建立合并过程。你需要决定代码的共享方式以及模块应该采用哪种命名约定。SHARED KERNEL的代码至少每周要集成一次,而且它必须有一个测试套件。在开发任何共享代码之前,先把它设置好。(测试套件将是空的,因此很容易通过!)
(3) 选择某个小的SUBDOAMIN作为开始,它应该是两个CONTEXT中重复出现的SUBDOAMIN,但不是CORE DOMAIN的一部分。最初的合并主要是为了建立合并过程,因此最好选择一些简单且相对通用或不重要的部分。检查已存在的集成和转换。选择那些经过转换的部分,其优势在于一开始就有用于验证的转换机制,此外还可以简化转换层。
此时,我们有两个应对相同子领域的模型。基本上有3种合并方法。(a)我们可以选择一个模型,并重构另一个CONTEXT,使之与第一个模型兼容。(b)我们可以从整体上做出这个决策,把目标设置为系统性地替换一个CONTEXT的模型,并保持被开发模型的内聚性。也可以一次选择一部分,到最后两个模型可能会"两全其美"(但注意最后不要弄得一团糟)。©第三种选择是找到一个新模型,这个模型可能比最初的两个都深刻,能够承担二者的职责。
(4) 从两个团队中共选出2~4位开发人员组成一个小组,由他们来为子领域开发一个共享的模型。不管模型是如何得出的,它的内容必须详细。这包括一些困难的工作:识别同义词和映射那些尚未被翻译的术语。这个联合团队需要为模型开发一个基本的测试集。
(5) 来自两个团队的开发人员一起负责实现模型(或修改要共享的现有代码)、确定各种细节并使模型开始工作。如果这些开发人员在模型中遇到了问题,就从第(3)步开始重新组织团队,并进行必要的概念修订工作。
(6) 每个团队的开发人员都承担与新的SHARED KERNEL集成的任务。
(7) 清除那些不再需要的翻译。
这时你会得到一个非常小的SHARED KERNEL,并且有一个过程来维护它。在后续的项目迭代中,重复第(3)~(7)步来共享更多内容。随着过程的不断巩固和团队信心的树立,就可以选择更复杂的SUBDOAMIN了,同时处理多个SUBDOAMIN,或者处理CORE DOMAIN中的SUBDOAMIN。
注意:当从模型中选取更多与领域有关的部分时,可能会遇到这样的情况,即两个模型各自采用了不同用户群的专用术语。聪明的做法是先不要把它们合并到SHARED KERNEL中,除非工作中出现了突破,得到了一个深层模型,这个模型为你提供了一种能够替代那两种专用术语的语言。SHARED KERNEL的优点是它具有CONTINUOUS INTEGRATION的部分优势,同时又保留了SEPARATE WAY模式的一些优点。
以上这些是把模型的一些部分合并到SHARED KERNEL中的指导原则。在继续讨论之前,我们来看一下另外一种方法,它能够部分解决上述转换所面对的问题。如果两个模型中有一个毫无疑问是符合首选条件的,那么就考虑向它过渡,而不用进行集成。不共享公共的SUBDOAMIN,而只是系统性地通过重构应用程序把这些SUBDOAMIN的所有职责从一个BOUNDED CONTEXT转移到另一个BOUNDED CONTEXT,从而使用那个更受青睐的CONTEXT的模型,并对该模型进行需要的增强。在没有集成开销的情况下,消除了冗余。很有可能(但也不是必然的)那个更受青睐的BOUNDED CONTEXT最终会完全取代另一个BOUNDED CONTEXT,这样就实现了与合并完全一样的效果。在转换过程中(这个过程可能相当长或无法确定),这种方法具有SEPARATE WAY模式常见的优点和缺点,而且我们必须拿这些优缺点与SHARED KERNEL的利弊进行权衡。
合并CONTEXT:SHARED KERNEL→CONTINUOUS INTEGRATION
如果你的SHARED KERNEL正在扩大,你可能会被完全统一两个BOUNDED CONTEXT的优点所吸引。但这并不只是一个解决模型差异的问题。你将改变团队的结构,而且最终会改变人们所使用的语言。
这个过程从人员和团队的准备开始。
(1) 确保每个团队都已经建立了CONTINUOUS INTEGRATION所需的所有过程(共享代码所有权、频繁集成等)。两个团队协商集成步骤,以便所有人都以同一步调工作。
(2) 团队成员在团队之间流动。这样可以形成一大批同时理解两个模型的人员,并且可以把两个团队的人员联系起来。
(3) 澄清每个模型的精髓(。
(4) 现在,团队应该有了足够的信心把核心领域合并到SHARED KERNEL中。这可能需要多次迭代,有时需要在新共享的部分与尚未共享的部分之间使用临时的转换层。一旦进入到合并CORE DOMAIN的过程中,最好能快速完成。这是一个开销高且易出错的阶段,因此应该尽可能缩短时间,要优先于新的开发任务。但注意量力而行,不要超过你的处理能力。
有几种方式用于合并CORE模型。可以保持一个模型,然后修改另一个,使之与第一个兼容,或者可以为子领域创建一个新模型,并通过修改两个上下文来使用这个模型。如果两个模型已经被修改以满足不同用户的需要,你就要注意了。你需要保留两个初始模型中的这些专业能力。这就要求开发一个能够替代两个原始模型的更深层的模型。开发这样一个更深入的统一模型是很难的,但如果你已经决定完全合并两个CONTEXT,就没有选择多种专门术语的空间了。这样做的好处是最终模型和代码的集成变得更清晰了。注意不要影响到你满足用户特殊需要的能力。
(5) 随着SHAREDKERNEL的增长,把集成频率提高到每天一次,最后实现CONTINUOUS INTEGRATION。
(6) 当SHARED KERNEL逐渐把先前两个BOUNDED CONTEXT的所有内容都包括进来的时候,你会发现要么形成了一个大的团队,要么形成了两个较小的团队,这两个较小的团队共享一个CONTINUOUS INTEGRATION的代码库,而且团队成员可以经常在两个团队之间来回流动。
逐步淘汰遗留系统
淘汰遗留系统涉及面太广了,这里的讨论也只能浅尝辄止。我们将讨论一种常见的情况:用一系列更现代的系统来补充业务中每天都在使用的老系统,新系统通过一个ANTICORRUPTION LAYER与老系统进行通信。
首先要执行的步骤是确定测试策略。应该为新系统中的新功能编写自动的单元测试,但逐步淘汰遗留系统还有一些特殊的测试需求。一些组织在某段时间内会同时运行新旧两个系统。
在任何一次迭代中:
(1) 确定遗留系统的哪个功能可以在一个迭代中被添加到某个新系统中;
(2) 确定需要在ANTICORRUPTION LAYER中添加的功能;
(3) 实现;
(4) 部署;
有时,需要进行多次迭代才能编写一个与遗留系统的某个功能等价的功能单元,这时在计划新的替代功能时仍以小规模的迭代为单元,最后一次性部署多次迭代。部署涉及的变数太多,以至于本文不可能涵盖所有的基本情况。就开发而言,如果这些小规模、增量的改动能够推到生产环境,那真是再好不过了。但通常情况,还是需要将他们组织成更大的发布。在新软件的使用方面,用户培训是必不可少的。有时在成功部署的同时还必须进行开发工作。还有很多后勤问题需要解决。
一旦最终进入运行阶段后,应该遵循如下步骤。
(5) 找出ANTICORRUPTION LAYER中那些不必要的部分,并去掉它们;
(6) 考虑删除遗留系统中目前未被使用的模块,虽然这种做法未必实际。有趣的是,遗留系统设计得越好,它就越容易被淘汰。而设计得不好的软件却很难一点儿一点儿地去除。这时,我们可以暂时忽略那些未使用的部分,直到将来剩余部分已经被淘汰,这时整个遗留系统就可以停止使用了。
不断重复这几个步骤。遗留系统应该越来越少地参与业务,最终,替换工作会看到希望的曙光并完全停止遗留系统。同时,随着各种组合增加或减小系统之间的依赖,ANTICORRUPTION LAYER将相应地收缩或扩张。当然,在其他条件都相同的情况下,应该首先迁移那些只产生较小ANTICORRUPTION LAYER的功能。但其他因素也可能会起主导作用,有时候在过渡期间可能必须经历一些麻烦的转换。
OPEN HOST SERVICE→PUBLISHED LANGUAGE
我们已经通过一系列特定的协议与其他系统进行了集成,但随着需要访问的系统逐渐增多,维护负担也不断增加,或者交互变得很难理解。我们需要通过PUBLISHED LANGUAGE来规范系统之间的关系。
(1) 如果有一种行业标准语言可用,则尽可能评估并使用它。
(2) 如果没有标准语言或预先公开发布的语言,则完善作为HOST的系统的CORE DOMAIN。
(3) 使用CORE DOMAIN作为交换语言的基础,尽可能使用像XML、JSON这样的标准交互范式。
(4) (至少)向所有参与协作的各方发布新语言。
(5) 如果涉及新的系统架构,那么也要发布它。
(6) 为每个协作系统构建转换层。
(7) 切换。
现在,当加入更多协作系统时,对整个系统的破坏已经减至最小了。
记住,PUBLISHED LANGUAGE必须是稳定的,但是当继续进行重构时,仍然需要能够自由地更改HOST的模型。因此,不要把交换语言和HOST的模型等同起来。保持它们的密切关系可以减小转换开销,而你的HOST可以采用CONFORMIST模式。但是应该保留对转换层进行补充的权力,在成本——效益的折中需要时,可以把这个权利分离出去。
项目领导者应该根据功能集成需求和开发团队之间的关系来定义BOUNDED CONTEXT。一旦BOUNDED CONTEXT和CONTEXT MAP被明确地定义下来并获得认可,就应该保持它们的逻辑一致性。最起码要把相关的通信问题提出来,以便解决它们。
但是,有时模型上下文(无论是我们有意识地划定边界的还是自然出现的上下文)被错误地用来解决系统中的一些其他问题,而不是逻辑不一致问题。团队可能会发现一个很大的CONTEXT的模型由于过于复杂而无法作为一个整体来理解或透彻地分析。出于有意或无意的考虑,团队往往会把CONTEXT分割为更易管理的部分。这种分割会导致失去很多机会。现在,值得花费一些功夫仔细考查在一个大的CONTEXT中建立一个大模型的决策了。如果从组织结构或行政角度来看保持一个大模型并不现实,如果实际上模型就是分裂的,那么就重新绘制CONTEXT MAP,并定义能够保持的边界。但是,如果保持一个大的BOUNDED CONTEXT能够解决迫切的集成需要,而且除了模型本身的复杂性以外,这看上去是行得通的,那么分割CONTEXT可能就不是最佳的选择了。在做出这种牺牲之前,还应该考虑其他一些能够使大模型变得易于管理的方法。下两章将着重讨论通过应用两种更广泛的原则(精炼和大型结构)来管理大模型的复杂性。
精炼(Distillation)
精炼是把一堆混杂在一起的组件分开的过程,从中提取出最重要的内容,使得它更有价值,也更有用。在软件设计中,精炼就是对模型中的关键方面进行抽象,或者是对大系统进行划分,从而把CORE DOMAIN(核心领域)提取出来。
如何才能专注于核心问题而不被大量的次要问题淹没呢?LAYERED ARCHITECTURE可以把领域概念从技术逻辑中(技术逻辑确保了计算机系统能够运转)分离出来,但在大型系统中,即使领域被分离出来,它的复杂性也可能仍然难以管理。
精炼是把一堆混杂在一起的组件分开的过程,以便通过某种形式从中提取出最重要的内容,而这种形式将使它更有价值,也更有用。模型就是知识的精炼。通过每次重构所得到的更深层的理解,我们得以把关键的领域知识和优先级提取出来。现在,让我们回过头来从战略角度看一下精炼,本章将介绍对模型进行粗线条划分的各种方式,并把领域模型作为一个整体进行精炼。
像很多化学蒸馏过程一样,精炼过程所分离出来的副产品(如GENERIC SUBDOMAIN和COHERENT MECHANISM)本身也很有价值,但精炼的主要动机是把最有价值的那部分提取出来,正是这个部分使我们的软件区别于其他软件并让整个软件的构建物有所值,这个部分就是CORE DOMAIN。
领域模型的战略精炼包括以下部分:
(1) 帮助所有团队成员掌握系统的总体设计以及各部分如何协调工作;
(2) 找到一个具有适度规模的核心模型并把它添加到UBIQUITOUS LANGUAGE中,从而促进沟通;
(3) 指导重构;
(4) 专注于模型中最有价值的那部分;
(5) 指导外包、现成组件的使用以及任务委派。
本章将展示对CORE DOMAIN进行战略精炼的系统性方法,解释如何在团队中有效地统一认识,并提供一种用于讨论工作的语言。
像那些园丁为了让树干快速生长而修剪树苗一样,我们将使用一整套技术把模型中那些细枝末节砍掉,从而把注意力集中在最重要的部分上…
模式:CORE DOMAIN
CORE DOMAIN(核心领域)——模型的独特部分,是用户的核心目标,它使得应用程序与众不同并且有价值。
在设计大型系统时,有非常多的组成部分——它们都很复杂而且对开发的成功也至关重要,但这导致真正的业务资产——领域模型最为精华的部分——被掩盖和忽略了。
难以理解的系统修改起来会很困难,而且修改的结果也难以预料。开发人员如果脱离自己熟悉的领域,也会迷失方向(当团队中有新人加入时尤其如此,但老成员也面临同样的状况,除非代码表达得非常清楚并且组织有序)。这样一来就必须分门别类地为人们安排任务。当开发人员把他们的工作限定到具体的模块时,知识的传递就更少了。这种工作上的划分导致系统很难平滑地集成,也无法灵活地分配工作。如果开发人员没有了解到某项功能已经被实现了,那么就会出现重复,这样系统会变得更加复杂。
以上只是难以理解的设计所导致的一部分后果。当失去了领域的整体视图时,还存在另一个同样严重的风险。
一个严峻的现实是我们不可能对所有设计部分进行同等的精化,而是必须分出优先级。为了使领域模型成为有价值的资产,必须整齐地梳理出模型的真正核心,并完全根据这个核心来创建应用程序的功能。但本来就稀缺的高水平开发人员往往会把工作重点放在技术基础设施上,或者只是去解决那些不需要专门领域知识就能理解的领域问题(这些问题都已经有了很好的定义)。
计算机科学家对系统的这些部分更感兴趣,他们认为通过这些工作可以让自己具备一些在其他地方也能派上用场的专业技能,同时也丰富了个人简历。而真正体现应用程序价值并且使之成为业务资产的领域核心却通常是由那些技术水平稍差的开发人员完成的,他们与DBA一起创建数据模式,然后逐个特性编写代码,而根本没有对模型的概念能力加以任何利用。
如果软件的这个部分实现得很差,那么无论技术基础设施有多好,无论支持功能有多完善,应用程序永远都不会为用户提供真正有吸引力的功能。这个严重问题的根源在于项目没有一个明确的整体设计视图,而且也没有认清各个部分的相对重要性。
在制定项目规划的时候,必须把资源分配给模型和设计中最关键的部分。要想达到这个目的,在规划和开发期间每个人都必须识别和理解这些关键部分。
这些部分是应用程序的标志性部分,也是目标应用程序的核心诉求,它们构成了CORE DOMAIN。CORE DOMAIN是系统中最有价值的部分。
对模型进行提炼。找到CORE DOMAIN并提供一种易于区分的方法把它与那些起辅助作用的模型和代码分开。最有价值和最专业的概念要轮廓分明。尽量压缩CORE DOMAIN。
让最有才能的人来开发CORE DOMAIN,并据此要求进行相应的招聘。在CORE DOMAIN中努力开发能够确保实现系统蓝图的深层模型和柔性设计。仔细判断任何其他部分的投入,看它是否能够支持这个提炼出来的CORE。
提炼CORE DOMAIN并不容易,但它确实会让一些决策变得容易。你需要投入大量的工作使你的CORE鲜明突出,而其他设计部分则只需依照常规做得实用即可。如果某个设计部分需要保密以便保持竞争优势,那么它就是你的CORE DOMAIN。其他的部分则没有必要隐藏起来。当必须在两个看起来都很有用的重构之间进行抉择时(由于时限的缘故),应该首选对CORE DOMAIN影响最大的那个重构。
本章中的模式能够使我们更容易发现、使用和修改CORE DOMAIN。
选择CORE DOMAIN
我们需要关注的是那些能够表示业务领域并解决业务问题的模型部分,也即CORE DOMAIN。
对CORE DOMAIN的选择取决于看问题的角度。例如,很多应用程序需要一个通用的货币模型,用来表示各种货币以及它们的汇率和兑换。另一方面,一个用来支持货币交易的应用程序可能需要更精细的货币模型,这个模型有可能就是CORE的一部分。即使在这种情况下,货币模型中可能有一部分仍是非常通用的。随着对领域理解的不断加深,精炼过程可以持续进行,这会把通用的货币概念分离出来,而只把模型中那些专有的部分保留在CORE DOMAIN中。
在运输应用程序中,CORE可能是以下几方面的模型:货物是如何装船运输的,当集装箱转交时责任是如何转接的,或者特定的集装箱是如何经由不同的运输路线最后到达目的地的。在投资银行中,CORE可能包括委托人和参与者之间的合资模型。
一个应用程序的CORE DOMAIN在另一个应用程序中可能只是通用的支持组件。尽管如此,仍然可以在一个项目中(而且通常在一个公司中)定义一个一致的CORE。像其他设计部分一样,人们对CORE DOMAIN的认识也会随着迭代而发展。开始时,一些特定关系可能显得不重要。而最初被认为是核心的对象可能逐渐被证明只是起支持作用。
下面几节(特别是GENERIC SUBDOMAIN这节)将给出制定这些决策的指导。
工作的分配
在项目团队中,技术能力最强的人员往往缺乏丰富的领域知识。这限制了他们的作用,并且更倾向于分派他们来开发一些支持组件,从而形成了一个恶性循环——知识的缺乏使他们远离了那些能够学到领域知识的工作。
打破这种恶性循环是很重要的,方法是建立一支由开发人员和一位或多位领域专家组成的稳定的联合团队,其中开发人员必须能力很强、能够长期稳定地工作并且对学习领域知识非常感兴趣,而领域专家则要掌握深厚的业务知识。如果你认真对待领域设计,那么它就是一项有趣且充满技术挑战的工作。你肯定也会找到持这种观点的开发人员。
从外界聘请一些短期的专业人员来设计CORE DOMAIN的关键环节通常是行不通的,因为团队需要积累领域知识,而且短期人员会造成知识流失。相反,充当培训和指导角色的专家可能非常有价值,因为他们帮助团队建立领域设计技巧,并促进团队成员使用尚未掌握的高级设计原则。
出于类似的原因,购买CORE DOMAIN也是行不通的。人们已经在建立特定于行业的模型框架方面付出了一些工作,著名的例子就是半导体行业协会SEMATECH创立的用于半导体制造自动化的CIM框架,以及IBM为很多业务开发的San Francisco框架。虽然这是一个有吸引力的想法,但除了能够促进数据交换的PUBLISHED LANGUAGE以外,其他结果并不理想。Domain-Specific Application Frameworks[Fayad and Johnson 2000]一书介绍了这项工作的总体状况。随着这个领域的进步,可能会出现一些更有用的框架。
除了上述原因之外,还有一个更重要的原因需要引起我们的注意。自主开发的软件的最大价值来自于对CORE DOMAIN的完全控制。一个设计良好的框架可能会提供满足你的专门使用需求的高水平抽象,它可以节省开发那些更通用部分的时间,并使你能够专注于CORE。但是,如果它对你的约束超出了这个限度,可能有以下3种原因。
(1) 你正在失去一项重要的软件资产。此时应该让这些限制性的框架退出你的CORE DOMAIN。
(2) 框架所处理的部分并不是你所认为的核心。此时应该重新划定CORE DOMAIN的边界,把你的模型中真正的标志性部分识别出来。
(3) 你的CORE DOMAIN并没有特殊的需求。此时应该考虑采用一种风险更低的解决方案,如购买软件并与你的应用程序进行集成。
不管是哪种情况,创建与众不同的软件还是会回到原来的轨道上——需要一支稳定工作的团队,他们不断积累和消化专业知识,并将这些知识转化为一个丰富的模型。没有捷径,也没有魔法。
精炼的逐步提升
本章接下来将要介绍各种精炼技术,它们在使用顺序上基本没什么要求,但对设计的改动却大不相同。
一份简单的DOMAIN VISION STATEMENT(领域愿景说明)只需很少的投入,它传达了基本概念以及它们的价值。HIGHLIGHTED CORE(突出的核心)可以增进沟通,并指导决策制定,这也只需对设计进行很少的改动甚至无需改动。
更积极的精炼方法是通过重构和重新打包显式地分离出GENERIC SUBDOMAIN,然后单独进行处理。在使用COHESIVE MECHANISM的同时,也要保持设计的通用性、易懂性和柔性,这两个方面可以结合起来。只有除去了这些细枝末节,才能把CORE剥离出来。重新打包出一个SEGREGATED CORE(分离的核心),可以使这个CORE清晰可见(即使在代码中也是如此),并且促进将来在CORE模型上的工作。
最富雄心的精炼是ABSTRACT CORE(抽象内核),它用纯粹的形式表示了最基本的概念和关系(因此,需要对模型进行全面的重新组织和重构)。
每种技术都需要我们连续不断地投入越来越多的工作,但刀磨得越薄,就会越锋利。领域模型的连续精炼将为我们创造一项资产,使项目进行得更快、更敏捷、更精确。
首先,我们可以把模型中最普通的那些部分分离出去,它们就是GENERIC SUBDOMAIN(通用子领域)。GENERIC SUBDOMAIN与CORE DOMAIN形成鲜明的对比,使我们可以更清楚地理解它们各自的含义。
模式:GENERIC SUBDOMAIN
模型中有些部分除了增加复杂性以外并没有捕捉或传递任何专门的知识。任何外来因素都会使CORE DOMAIN愈发的难以分辨和理解。模型中充斥着大量众所周知的一般原则,或者是专门的细节,这些细节并不是我们的主要关注点,而只是起到支持作用。然而,无论它们是多么通用的元素,它们对实现系统功能和充分表达模型都是极为重要的。
模型中有你想当然的部分。不可否认,它们确实是领域模型的一部分,但它们抽象出来的概念是很多业务都需要的。比如,各个行业(如运输业、银行业或制造业)都需要某种形式的企业组织图。再比如,很多应用程序都需要跟踪应收账款、开支分类账和其他财务事项,而这些都可以用一个通用的会计模型来处理。
通常,人们投注了大量精力去处理领域的周边问题。我亲眼目睹过两个不同项目都分派了最好的开发人员来重新设计带有时区的日期和时间功能,这些工作耗费了他们数周的时间。虽然这样的组件必须正常工作,但它们并不是系统的概念核心。
即使这样的通用模型元素确实非常重要,整个领域模型仍然需要把系统中最有价值和最特别的方面突出出来,而且整个模型的组织应该尽可能把重点放在这个部分上。当核心与所有相关的因素混杂在一起时,这一点会更难做到。
识别出那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并放到单独的MODULE中。任何专有的东西都不应放在这些模块中。
把它们分离出来以后,在继续开发的过程中,它们的优先级应低于CORE DOMAIN的优先级,并且不要分派核心开发人员来完成这些任务(因为他们很少能够从这些任务中获得领域知识)。此外,还可以考虑为这些GENERIC SUBDOMAIN使用现成的解决方案或"公开发布的模型"(PUBLISHED MODEL)。
GENERIC SUBDOMAIN是你充分利用外部设计专家的地方,因为这些专家不需要深入理解你特有的CORE DOMAIN,而且他们也没有太大的机会学习这个领域。机密性问题可以不用过多关注,因为这些模块几乎不涉及专有信息或业务实践。GENERIC SUBDOMAIN可以减轻对那些不了解领域知识的人员进行培训而带来的负担。
随着时间的推移,CORE模型的范围将会不断变窄,而越来越多的通用模型将作为框架被实现出来,或者至少被实现为公开发布的模型或分析模式。但是现在,大部分模型仍然需要我们自己开发,但把它们与CORE DOMAIN模型区分开是很有价值的。
使用现成的解决方案
有时可以购买一个已实现好的解决方案,或使用开源代码。
优点
可以减少代码的开发。
维护负担转移到了外部。
代码已经在很多地方使用过,可能较为成熟,因此比自己开发的代码更可靠和完备。
缺点
在使用之前,仍需要花时间来评估和理解它。
就业内目前的质量控制水平而言,无法保证它的正确性和稳定性。
它可能设计得过于细致了(远远超出了你的目的),集成的工作量可能比开发一个最小化的内部实现更大。
外部元素的集成常常不顺利。它可能有一个与你的项目完全不同的BOUNDED CONTEXT。即使不是这样,它也很难顺利地引用你的其他软件包中的ENTITY。
它可能会引入对平台、编译器版本的依赖等。
现成的子领域解决方案是值得我们去考虑的,但如果它们常常会带来麻烦,那么往往就得不偿失了。我曾经看到过一些成功案例——一些应用程序需要非常精细的工作流,它们通过API挂钩(API hook)成功地使用了商用的外部工作流系统。我曾经还见过错误日志被深入地集成到应用程序中。有时,GENERIC SUBDOMAIN被打包为框架的形式,它实现了非常抽象的模型,从而可以与你的应用程序集成来满足你的特殊需求。子组件越通用,其自己的模型的精炼程度越高,它的用处可能就越大。
公开发布的设计或模型
优点
比自己开发的模型更为成熟,并且反映了很多人的深层知识。
提供了随时可用的高质量文档。
缺点
可能不是很符合你的需要,或者设计得过于细致了(远远超出了你的需要)。
Tom Lehrer(20世纪50和60年代的喜剧作曲家)曾经讲过数学上的成功秘诀是:"抄袭!抄袭。不要让任何人的工作逃过你的眼睛…但一定要记得深入研究。"在领域建模中,特别是在攻克GENERIC SUBDOMAIN时,这是金玉良言。
当有一个被广泛使用的模型时,如《分析模式》[Fowler 1996]一书中所列举的那些模型(参见第11章),这种方法最为有效。
如果领域中已经有了一种非常正式且严格的模型,那么就使用它。会计和物理学是我们立即能想到的两个例子。这些模型不仅精简和健壮,而且被人们广泛理解,因此可以减轻目前和将来的培训负担。
如果在一个公开发布的模式中能够发现一个简化的子集,它本身是一致的而且能够满足你的要求,那么就不要强迫自己完全实现一个这样的模型。如果一个模型已经有人很好地研究过了,并且提供了完备的文档,甚至已经得到正规化,那么重新去设计它就没有意义了。
把实现外包出去
优点
使核心团队可以脱身去处理CORE DOMAIN,那才是最需要知识和经验积累的部分。
开发工作的增加不会使团队规模无限扩大下去,同时又不会导致CORE DOMAIN知识的分散。
强制团队采用面向接口的设计,并且有助于保持子领域的通用性,因为规格已经被传递到外部。
缺点
仍需要核心团队花费一些时间,因为他们需要与外包人员商量接口、编码标准和其他重要方面。
当把代码移交回团队时,团队需要耗费大量精力来理解这些代码。(但是这个开销比理解专用子领域要小一些,因为通用子领域不需要理解专门的背景知识。)
代码质量或高或低,这取决于两个团队能力的高低。
自动测试在外包中可能起到重要作用。应该要求外包人员为他们交付的代码提供单元测试。真正有用的方法是为外包的组件详细说明甚至是编写自动验收测试,这有助于确保质量、明确规格并且使这些组件的再集成变得顺利。此外,"把实现外包出去"能够与"公开发布的设计或模型"完美地组合到一起。
内部实现
优点
易于集成。
只开发自己需要的,不做多余的工作。
可以临时把工作分包出去。
缺点
需要承受后续的维护和培训负担。
很容易低估开发这些软件包所需的时间和成本。当然,这也可以与“公开发布的设计或模型”结合起来使用。
通用不等于可重用
注意,虽然本文一直在强调这些子领域的通用性,但并没有提代码的可重用性。现成的解决方案可能适用于某种特殊情况,也可能不适用,但假设你要自己实现代码(内部实现或外包出去),那么不要特别关注代码的可重用性。因为那样做会违反精炼的基本动机——我们应该尽可能把大部分精力投入到CORE DOMAIN工作中,而只在必要的时候才在支持性的GENERIC SUBDOMAIN中投入工作。
重用确实会发生,但不一定总是代码重用。模型重用通常是更高级的重用,例如,当使用公开发布的设计或模型的时候就是如此。如果你必须创建自己的模型,那么它在以后的相关项目中可能很有价值。但是,虽然这样的模型概念可能适用于很多情况,我们也不必把它开发成"万能的"模型。我们只要把业务所需的那部分建模出来并实现即可。
尽管我们很少需要考虑设计的可重用性,但通用子领域的设计必须严格地限定在通用概念的范围之内。如果把行业专用的模型元素引入到通用子领域中,会产生两个后果。
第一,它会妨碍将来的开发。虽然现在我们只需要子领域模型的一小部分,但我们的需求会不断增加。如果把任何不属于子领域概念的部分引入到设计中,那么再想灵活地扩展系统就很难了,除非完全重建原来的部分并重新设计使用该部分的其他模块。
第二,也是更重要的,这些行业专用的概念要么属于CORE DOMAIN,要么属于它们自己的更专业的子领域,而且这些专业的模型比通用子领域更有价值。
项目风险管理
敏捷过程通常要求通过尽早解决最具风险的任务来管理风险。特别是XP过程,它要求迅速建立并运行一个端到端的系统。这种初步的系统通常用来检验某种技术架构,而且人们会试图建立一个外围系统,用来处理一些支持性的GENERIC SUBDOMAIN,因为这些子领域通常更易于分析。但是要注意,这可能会不利于风险管理。
项目面临着两方面的风险,有些项目的技术风险更大,有些项目则是领域建模的风险更大一些。端到端的系统是实际系统中最困难部分的"雏形"——它控制风险的能力也仅限于此。当使用这种雏形时,我们很容易低估领域建模的风险。这种风险包括未预料到存在复杂性、与业务专家的交流不够充分,或者开发人员的关键技能存在欠缺等。因此,除非团队拥有精湛的技术并且对领域非常熟悉,否则第一个雏形系统应该以CORE DOMAIN的某个部分作为基础,不管它有多么简单。相同的原则也适用于任何试图把高风险的任务放到前面处理的过程。CORE DOMAIN就是高风险的,因为它的难度往往会超出我们的预料,而且如果没有它,项目就不可能获得成功。
本章介绍的大多数精炼模式都展示了如何修改模型和代码,以便提炼出CORE DOMAIN。但是,接下来的两个模式DOMAIN VISION STATEMENT和HIGHLIGHTED CORE将展示如何用最少的投入通过补充文档来增进沟通、提高人们对CORE的认识并使之把开发工作集中到CORE上来…
模式:DOMAIN VISION STATEMENT
在项目开始时,模型通常并不存在,但是模型开发的需求是早就确定下来的重点。在后面的开发阶段,我们需要解释清楚系统的价值,但这并不需要深入地分析模型。此外,领域模型的关键方面可能跨越多个BOUNDED CONTEXT,而且从定义上看,无法将这些彼此不同的模型组织起来表明其共同的关注点。
很多项目团队都会编写"VISION STATEMENT(愿景说明)“以便管理。最好的VISION STATEMENT会展示出应用程序为组织带来的具体价值。一些VISION STATEMENT会把创建领域模型当作一项战略资产。通常,VISION STATEMENT文档在项目启动以后就被弃之不用了,而在实际开发过程中从来不会使用它,甚至根本不会有技术人员去阅读它。
DOMAIN VISION STATEMENT(领域愿景说明)就是模仿这类文档创建的,但它关注的重点是领域模型的本质,以及如何为企业带来价值。在项目开发的所有阶段,管理层和技术人员都可以直接用DOMAIN VISION STATEMENT来指导资源分配、建模选择和团队成员的培训。如果领域模型为多个群体提供服务,那么此文档还能够显示出他们的利益是如何均衡的。
写一份CORE DOMAIN的简短描述(大约一页纸)以及它将会创造的价值,也就是"价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方利益的。这份描述要尽量精简。尽早把它写出来,随着新的理解随时修改它。
DOMAIN VISION STATEMENT可以用作一个指南,它帮助开发团队在精炼模型和代码的过程中保持统一的方向。团队中的非技术成员、管理层甚至是客户也都可以共享DOMAIN VISION STATEMENT(当然,包含专有信息的情况除外)。
DOMAIN VISION STATEMENT为团队提供了统一的方向。但在高层次的说明和代码或模型的完整细节之间通常还需要做一些衔接…
模式:HIGHLIGHTED CORE
DOMAIN VISION STATEMENT从宽泛的角度对CORE DOMAIN进行了说明,但它把什么是具体CORE DOMAIN元素留给人们自己去解释和猜测。除非团队的沟通极其充分,否则单靠VISION STATEMENT是很难产生什么效果的。
尽管团队成员可能大体上知道CORE DOMAIN是由什么构成的,但CORE DOMAIN中到底包含哪些元素,不同的人会有不同的理解,甚至同一个人在不同的时间也会有不同的理解。如果我们总是要不断过滤模型以便识别出关键部分,那么就会分散本应该投入到设计上的精力,而且这还需要广泛的模型知识。因此,CORE DOMAIN必须要很容易被分辨出来。
对代码所做的重大结构性改动是识别CORE DOMAIN的理想方式,但这些改动往往无法在短期内完成。事实上,如果团队的认识还不够全面,这样的重大代码修改是很难进行的。
通过修改模型的组织结构(如划分GENERIC SUBDOMAIN和本章后面要介绍的一些改动),可以用MODULE表达出核心领域。但如果把它作为表达CORE DOMAIN的唯一方法,那么对模型的改动会很大,因此很难马上看到结果。
我们可能需要用一种轻量级的解决方案来补充这些激进的技术手段。可能有一些约束使你无法从物理上分离出CORE,或者你可能是从已有代码开始工作的,而这些代码并没有很好地区分出CORE,但你确实很需要知道什么是CORE并建立起共识,以便有效地通过重构进行更好的精炼。即使到了高级阶段,通过仔细挑选几个图或文档,也能够为团队提供思考的定位点和切入点。
无论是使用了详尽的UML模型的项目,还是那些只使用很少的外部文档并且把代码用作主要的模型存储库的项目(如XP项目),都会面临这些问题。极限编程团队可能采用更简洁的做法,他们更少地使用这些补充解决方案,而且只是临时使用(例如,在墙上挂一张手绘的图,让所有人都能看到),但这些技术可以很好地结合到开发过程中。
把模型的一个特别部分连同它的实现一起区分出来,这只是对模型的一种反映,而不必是模型自身的一部分。任何使人们易于了解CORE DOMAIN的技术都可以采用。这类解决方案有两种典型的代表性技术。
精炼文档
我经常会创建一个单独的文档来描述和解释CORE DOMAIN。这个文档可能很简单,只是最核心的概念对象的清单。它可能是一组描述这些对象的图,显示了它们最重要的关系。它可能在抽象层次上或通过示例来描述基本的交互过程。它可能会使用UML类图或序列图、专用于领域的非标准的图、措辞严谨的文字解释或上述这些元素的组合。精炼文档并不是完备的设计文档。它只是一个最简单的切入点,描述并解释了核心,并给出了更进一步研究这些核心部分的理由。精炼文档为读者提供了一个总体视图,指出了各个部分是如何组合到一起的,并且指导读者到相应的代码部分寻找更多细节。
因此(作为HIGHLIGHTED CORE(突出核心)的一种形式):
编写一个非常简短的文档(3~7页,每页内容不必太多),用于描述CORE DOMAIN以及CORE元素之间的主要交互过程。
独立文档带来的所有常见风险也会在这里出现:
(1) 文档可能得不到维护;
(2) 文档可能没人阅读;
(3) 由于有多个信息来源,文档可能达不到简化复杂性的目的。
控制这些风险的最好方法是保持绝对的精简。剔除那些不重要的细节,只关注核心抽象以及它们的交互,这样文档的老化速度就会减慢,因为这个层次的模型通常更稳定。
精炼文档应该能够被团队中的非技术人员理解。把它当作一个共享的视图,描述每个人都应该知道的东西,而且可以把它作为团队所有成员研究模型和代码的一个起点。
15.5.2 标明CORE
我以前参加过一家大型保险公司的项目,在上班的第一天,有人给了我一份200页的"领域模型"文档的复印件,这个文档是花高价从一家行业协会购买的。我花了几天时间仔细研究了一大堆类图,它们涵盖了所有细节,从详细的保险政策组合到人们之间极为抽象的关系模型。这些模型的质量也参差不齐,有的只有高中生的水平,有的却相当好(有几个甚至描述了业务规则,至少在附带的文本中做了描述)。但我要从哪里开始工作呢?要知道它有200页啊。
这个项目的人员热衷于构建抽象框架,我的前任们非常关注人与人之间、人与事物之间以及人与活动或协议之间的抽象关系模型。他们确实对关系进行了很好的分析,而且模型实验也达到了专业研究项目的水准,但却并没有使我们找到开发这个保险应用程序的任何思路。
我对它的第一反应就是大幅删减,找到一个小的CORE DOMAIN并重构它,然后再逐步添加其他细节。但我的这个观点使管理层感到担心。这份文档具有极大的权威性。它是由整个行业的专家们编写的,而且无论如何他们付给协会的费用远远超过付给我的费用,因此他们不太可能慎重考虑我所提出的要进行彻底修改的建议。但我知道必须有一个共享的CORE DOMAIN视图,并让每个人的工作都以它为中心。
我没有进行重构,而是走查了文档,并且还得到了一位既懂得大量保险业一般知识又了解我们这个特殊应用程序的具体需求的业务分析师的帮助,把那些体现出基本的、区别于其他系统概念的部分标识出来,这些是我们真正需要处理的部分。我提供了一个模型的导航图,它清晰地显示了核心,以及它与支持特性的关系。
我们从这个角度开始了建立原型的新工作,很快就开发出了一个简化的应用程序,它展示了一些必需的功能。这沓两磅重的再生纸变成了一项有用的业务资产,而我做的只是加了少量的页标和一些黄色标记。
这种技术并不仅限于纸面上的对象图。使用大量UML图的团队可以使用一个"原型"(Stereotype)来识别核心元素。把代码用作唯一模型存储库的团队可以使用注释(可以采用Java Doc这样的结构),或使用开发环境中的一些工具。使用哪种特定技术都没关系,只要使开发人员容易分辨出什么在核心领域内,什么在核心领域外就可以了。
因此(作为另一种形式的HIGHLIGHTED CORE):
把模型的主要存储库中的CORE DOMAIN标记出来,不用特意去阐明其角色。使开发人员很容易就知道什么在核心内,什么在核心外。
现在,我们只做了很少的处理和维护工作,负责处理模型的人员就已经清晰地看到CORE DOMAIN了,至少模型已经被整理得很好,使人们很容易分清各个部分的组成。
15.5.3 把精炼文档作为过程工具
理论上,在XP项目上工作的任何结对成员(两位一起工作的程序员)都可以修改系统中的任何代码。但在实际中,一些修改会产生很大影响,因此需要更多的商量和协调。按照项目通常的组织形式,当在基础设施层中工作时,变更的影响可能很清楚;但在领域层中,影响就不那么明显了。
从CORE DOMAIN的概念来看,这种影响会变得清楚。更改CORE DOMAIN模型会产生较大的影响。对广泛使用的通用元素进行修改可能要求更新大量的代码,但不会像CORE DOMAIN修改那样产生概念上的变化。
把精炼文档作为一个指南。如果开发人员发现精炼文档本身需要修改以便与他们的代码或模型修改保持同步,那么这样的修改需要大家一起协商。这种修改要么是从根本上修改CORE DOMAIN元素或关系;要么是修改CORE DOMAIN的边界,把一些元素包含进来,或是把一些元素排除出去。不管使用什么沟通渠道(包括新版本的精炼文档的分发),模型的修改都必须传达到整个团队。
如果精炼文档概括了CORE DOMAIN的核心元素,那么它就可以作为一个指示器——用以指示模型改变的重要程度。当模型或代码的修改影响到精炼文档时,需要与团队其他成员一起协商。当对精炼文档做出修改时,需要立即通知所有团队成员,而且要把新版本的文档分发给他们。CORE外部的修改或精炼文档外部的细节修改则无需协商或通知,可以直接把它们集成到系统中,其他成员在后续工作过程中自然会看到这些修改。这样开发人员就拥有了XP所建议的完全的自治性。
尽管VISION STATEMENT和HIGHLIGHTED CORE可以起到通知和指导的作用,但它们本身并没有修改模型或代码。具体地划分GENERIC SUBDOMAIN可以除去一些非核心元素。接下来的几个模式着眼于从结构上修改模型和设计本身,目的是使CORE DOMAIN更明显,更易于管理。
模式:COHESIVE MECHANISM
封装机制是面向对象设计的一个基本原则。把复杂算法隐藏到方法中,再为方法起一个一看就知道其用途的名字,这样就把"做什么"和"如何做"分开了。这种技术使设计更易于理解和使用。然而它也有一些先天的局限性。
计算有时会非常复杂,使设计开始变得膨胀。机械性的"如何做"大量增加,把概念性的"做什么"完全掩盖了。为解决问题提供算法的大量方法掩盖了那些用于表达问题的方法。
这种方法的扩散是模型出问题的一种症状。这时应该通过重构得到更深层的理解,从而找到更适合解决问题的模型和设计元素。首先要寻找的解决方案是找到一个能使计算机制变得简单的模型。但有时我们会发现,有些计算机制本身在概念上就是内聚的。这种内聚的计算概念可能并不包括我们所需的全部计算。我们讨论的也不是一种万能的计算器。把内聚部分提取出来会使剩下的部分更易于理解。
把概念上的COHESIVE MECHANISM(内聚机制)分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。用一个INTENTION-REVEALING INTERFACE来暴露这个框架的功能。现在,领域中的其他元素就可以只专注于如何表达问题(做什么)了,而把解决方案的复杂细节(如何做)转移给了框架。
然后,这些被分离出来的机制承担起支持的任务,从而留下一个更小的、表达得更清楚的CORE DOMAIN,这个核心以更加声明式的方式通过接口来使用这些机制。
把标准的算法或公式识别出来以后,可以把一部分设计的复杂性转移到一系列已经过深入研究的概念中。在这种方法的引导下,我们可以放心地实现一个解决方案,而且只需进行很少的尝试和改错。我们可以依靠其他一些了解这种算法或至少能够查到相关资料的开发人员。这个好处类似于从公开发布的GENERIC SUBDOMAIN模型获得的好处,但找到完备的算法或公式计算的机会比利用通用子领域模型的机会更大一些,因为这种水平的计算机科学已经有了较深入的研究。但是,我们仍常常需要创建新的算法。创建的算法应该主要用于计算,避免在算法中混杂用于表达问题的领域模型。二者的职责应该分离。CORE DOMAIN或GENERIC SUBDOMAIN的模型描述的是事实、规则或问题。而COHESIVE MECHANISM则用来满足规则或者用来完成模型指定的计算。
COHESIVE MECHANISM的另一个例子是用一个框架来构造SPECIFICATION对象,并为这些对象所需的基本的比较和组合操作提供支持。利用这个框架,CORE DOMAIN和GENERIC SUBDOMAIN可以用SPECIFICATION模式中所描述的清晰的、易于理解的语言来声明它们的规格。这样,比较和组合等复杂操作可以留给框架去完成。
GENERIC SUBDOMAIN与COHESIVE MECHANISM的比较
GENERIC SUBDOMAIN与COHESIVE MECHANISM的动机是相同的——都是为CORE DOMAIN减负。区别在于二者所承担的职责的性质不同。GENERIC SUBDOMAIN是以描述性的模型作为基础的,它用这个模型表示出团队会如何看待领域的某个方面。在这一点上它与CORE DOMAIN没什么区别,只是重要性和专门程度较低而已。COHESIVE MECHANISM并不表示领域,它的目的是解决描述性模型所提出来的一些复杂的计算问题。
模型提出问题,COHESIVE MECHANISM解决问题。
在实践中,除非你识别出一种正式的、公开发布的算法,否则这种区别通常并不十分清楚,至少在开始时是这样。在后续的重构中,如果发现一些先前未识别的模型概念会使这种机制变得更为简单,那么就可以把这种算法精炼成一种更纯粹的机制,或者转换为一个GENERIC SUBDOMAIN。
MECHANISM是CORE DOMAIN一部分
我们几乎总是想要把MECHANISM从CORE DOMAIN中分离出去。例外的情况是MECHANISM本身就是专有的并且是软件的一项核心价值。有时,非常专用的算法就是这种情况。例如,如果一个非常高效的算法(用于计算日程安排)是运输物流应用程序中的标志性特性之一,那么该机制就可以被认为是概念核心的一部分。通过更深入的分析可能会得到一个更深层的模型,从而用一种封装的解决机制把这些规则显式地表达出来。
但那只是将来要做的进一步改进。是否做这个决定取决于成本——效益分析。实现新设计的难度有多大?当前设计有多难理解和修改?采用更高级的设计后,对从事这些工作的人来说,设计会得到多大程度的简化?当然,有人对新模型的组成有什么想法吗?
通过精炼得到声明式风格
声明式设计和"声明式风格"是上一个章的一个主题,但在本章的战略精炼这个话题上,有必要特别提一下这种设计风格。**精炼的价值在于使你能够看到自己正在做什么,不让无关细节分散你的注意力,并通过不断削减得到CORE DOMAIN。**如果领域中那些起到支持作用的部分提供了一种简练的语言,可用于表示CORE的概念和规则,同时又能够把计算或实施这些概念和规则的方式封装起来,那么CORE DOMAIN的重要部分就可以采用声明式设计。
COHESIVE MECHANISM用途最大的地方是它通过一个INTENTION-REVEALING INTERFACE来提供访问,并且具有概念上一致的ASSERTION和SIDE-EFFECT-FREE FUNCTION。利用这些MECHANISM和柔性设计,CORE DOMAIN可以使用有意义的声明,而不必调用难懂的函数。但最不同寻常的回报来自于使CORE DOMAIN的一部分产生突破,得到一个深层模型,而且这部分核心领域本身成为了一种语言,可以灵活且精确地表达出最重要的应用场景。
深层模型往往与相对应的柔性设计一起产生。柔性设计变得成熟的时候,就可以提供一组易于理解的元素,我们可以明确地把它们组合到一起来完成复杂的任务,或表达复杂的信息,就像单词组成句子一样。此时,客户代码就可以采用声明式风格,而且更为精炼。
把GENERIC SUBDOMAIN提取出来可以减少混乱,而COHESIVE MECHANISM可以把复杂操作封装起来。这样可以得到一个更专注的模型,从而减少了那些对用户活动没什么价值的、分散注意力的方面。但我们不太可能为领域模型中所有非CORE元素安排一个适当的去处。SEGREGATED CORE采用直接的方法从结构上把CORE DOMAIN划分出来。
模式:SEGREGATED CORE
模型中的元素可能有一部分属于CORE DOMAIN,而另一部分起支持作用。核心元素可能与一般元素紧密耦合在一起。CORE的概念内聚性可能不是很强,看上去也不明显。这种混乱性和耦合关系抑制了CORE。设计人员如果无法清晰地看到最重要的关系,就会开发出脆弱的设计。
通过把GENERIC SUBDOMAIN提取出来,可以从领域中清除一些干扰性的细节,使CORE变得更清楚。但识别和澄清所有这些子领域是很困难的工作,而且有些工作看起来并不值得去做。同时,最重要的CORE DOMAIN仍然与剩下的那些元素纠缠在一起。
对模型进行重构,把核心概念从支持性元素(包括定义不清楚的那些元素)中分离出来,并增强CORE的内聚性,同时减少它与其他代码的耦合。把所有通用元素或支持性元素提取到其他对象中,并把这些对象放到其他的包中——即使这会把一些紧密耦合的元素分开。
这里基本上采用了与GENERIC SUBDOMAIN一样的原则,只是从另一个方向来考虑而已。那些在应用程序中非常关键的内聚子领域可以被识别出来,并分离到它们自己的内聚包中。如何处理剩下那些未加区分的元素虽然也很重要,但其重要性略低。这些元素或多或少地可以保留在原先的位置,也可以放到包含了重要类的包中。最后,越来越多的剩余元素可以被提取到GENERIC SUBDOMAIN中。但就目前来看,使用哪种简单解决方案都可以,只需把注意力集中在SEGREGATED CORE(分离的核心)上即可。
通过重构得到SEGREGATED CORE的一般步骤如下所示。
(1) 识别出一个CORE子领域(可能是从精炼文档中得到的)。
(2) 把相关的类移到新的MODULE中,并根据与这些类有关的概念为模块命名。
(3) 对代码进行重构,把那些不直接表示概念的数据和功能分离出来。把分离出来的元素放到其他包的类(可以是新的类)中。尽量把它们与概念上相关的任务放在一起,但不要为了追求完美而浪费太长时间。把注意力放在提炼CORE子领域上,并且使CORE子领域对其他包的引用变得更明显且易于理解。
(4) 对新的SEGREGATED CORE MODULE进行重构,使其中的关系和交互变得更简单、表达得更清楚,并且最大限度地减少并澄清它与其他MODULE的关系(这将是一个持续进行的重构目标)。
(5) 对另一个CORE子领域重复这个过程,直到完成SEGREGATED CORE的工作。
创建SEGREGATED CORE的代价
有时候,把CORE分离出来会使得它与那些紧密耦合的非CORE类的关系变得更晦涩,甚至更复杂,但CORE DOMAIN更清晰了,而且更易于处理,因此获得的好处还是足以抵偿这种代价。
SEGREGATED CORE使我们能够提高CORE DOMAIN的内聚性。我们可以使用很多有意义的方式来分解模型,有时在创建SEGREGATED CORE时,可以把一个内聚性很好的MODULE拆分开,通过牺牲这种内聚性来换取CORE DOMAIN的内聚性。这样做是值得的,因为企业软件的最大价值来自于模型中企业的那些特有方面。
当然,另一个代价是分离CORE需要付出很大的工作量。我们必须认识到,在做出SEGREGATED CORE的决定时,有可能需要开发人员对整个系统做出修改。
当系统有一个很大的、非常重要的BOUNDED CONTEXT时,但模型的关键部分被大量支持性功能掩盖了,那么就需要创建SEGREGATED CORE了。
不断发展演变的团队决策
就像很多战略设计决策所要求的一样,创建SEGREGATED CORE需要整个团队一致行动。这一行动需要团队的一致决策,而且团队必须足够自律和协调才能执行这样的决策。困难之处在于既要约束每个人使其都使用相同的CORE定义,又不能一成不变地去执行这个决策。由于CORE DOMAIN也是不断演变的(像任何其他设计方面一样),在处理SEGREGATED CORE的过程中我们会不断积累经验,这将使我们对什么是核心什么是支持元素这些问题产生新的理解。我们应该把这些理解反馈到设计中,从而得到更完善的CORE DOMAIN和SEGREGATED CORE MODULE的定义。
这意味着新的理解必须持续不断地在整个团队中共享,但个人不能单方面根据这些理解擅自采取行动。无论团队采用了什么样的决策过程,团队一致通过也好,由领导者下命令决定也好,决策过程都必须具有足够的敏捷性,可以反复纠正。团队必须进行有效的沟通,以便使每个人都共享同一个CORE视图。
模式:ABSTRACT CORE
通常,即便是CORE DOMAIN模型也会包含太多的细节,以至于它很难表达出整体视图。
我们处理大模型的方法通常是把它分解为足够小的子领域,以便能够掌握它们并把它们放到一些独立的MODULE中。这种简化式的打包风格通常是行之有效的,能够使一个复杂的模型变得易于管理。但有时创建独立的MODULE反而会使子领域之间的交互变得晦涩难懂,甚至变得更复杂。
当不同MODULE的子领域之间有大量交互时,要么需要在MODULE之间创建很多引用,这在很大程度上抵消了划分模块的价值;要么就必须间接地实现这些交互,而后者会使模型变得晦涩难懂。
我们不妨考虑采用横向切割而不是纵向切割的方式。多态性(polymorphism)允许我们忽略抽象类型实例的很多细节变化。如果MODULE之间的大部分交互都可以在多态接口这个层次上表达出来,那么就可以把这些类型重构到一个特定的CORE MODULE中。
这里并不是寻找技术上的技巧。只有当领域中的基本概念能够用多态接口来表达时,这才是一种有价值的技术。在这种情况下,把这些分散注意力的细节分离出来可以使MODULE解耦,同时可以精炼出一个更小、更内聚的CORE DOMAIN。
把模型中最基本的概念识别出来,并分离到不同的类、抽象类或接口中。设计这个抽象模型,使之能够表达出重要组件之间的大部分交互。把这个完整的抽象模型放到它自己的MODULE中,而专用的、详细的实现类则留在由子领域定义的MODULE中。
现在,大部分专用的类都将引用ABSTRACT CORE MODULE,而不是其他专用的MODULE。ABSTRACT CORE(抽象核心)提供了主要概念及其交互的简化视图。
提取ABSTRACT CORE并不是一个机械的过程。例如,如果把MODULE之间频繁引用的所有类都自动移动到一个单独的MODULE中,那么结果可能是一团糟,而且毫无意义。对ABSTRACT CORE进行建模需要深入理解关键概念以及它们在系统的主要交互中扮演的角色。换言之,它是通过重构得到更深层理解的。而且它通常需要大量的重新设计。
如果项目中同时使用了ABSTRACT CORE和精炼文档,而且精炼文档随着应用程序理解的加深而不断演变,那么ABSTRACT CORE的最后结果看起来应该与精炼文档非常类似。当然,ABSTRACT CORE是用代码编写的,因此更为严格和完整。
深层模型精炼
精炼并不仅限于从整体上把领域中的一些部分从CORE中分离出来。它也意味着对子领域(特别是CORE DOMAIN)进行精炼,通过持续重构得到更深层的理解,从而向深层模型和柔性设计推进。精炼的目标是把模型设计得更明显,使我们可以用模型简单地把领域表示出来。深层模型把领域中最本质的方面精炼成一些简单的元素,使我们可以把这些元素组合起来解决应用程序中的重要问题。
尽管任何带来深层模型的突破都有价值,但只有CORE DOMAIN中的突破才能改变整个项目的轨道。
选择重构目标
当你遇到一个杂乱无章的大型系统时,应该从哪里入手呢?在XP社区中,答案往往是以下之一:
(1) 可以从任何地方开始,因为所有的东西都要进行重构;
(2) 从影响你工作的那部分开始——也就是完成具体任务所需要的那个部分。
这两种做法我都不赞成。第一种做法并不十分可行,只有少数完全由顶尖的程序员组成的团队才是例外。第二种做法往往只是对外围问题进行了处理,只治其标而不治其本,回避了最严重的问题。最终这会使代码变得越来越难以重构。
因此,如果你既不能全面解决问题,又不能"哪儿痛治哪儿",那么该怎么办呢?
(1) 如果采用"哪儿痛治哪儿"这种重构策略,要观察一下根源问题是否涉及CORE DOMAIN或CORE与支持元素的关系。如果确实涉及,那么就要接受挑战,首先修复CORE DOMAIN。
(2) 当可以自由选择重构的部分时,应首先集中精力把CORE DOMAIN更好地提取出来,完善对CORE的分离,并且把支持性的子领域提炼成通用子领域。
以上就是如何从重构中获取最大利益的方法。
大型结构(Large-Scale Structure)
大型结构——一组高层的概念和/或规则,它为整个系统建立了一种设计模式。它使人们能够从大的角度来讨论和理解系统。
硅谷一家小设计公司签了一份为卫星通信系统创建模拟器的合同。工作进展得很顺利,他们正在开发一个MODEL-DRIVEN DESIGN,这个设计能够表示和模拟各种网络条件和故障。
但开发团队的领导者却有点不安。问题本身太复杂了。为了澄清模型中的复杂关系,他们已经把设计分解为一些在规模上便于管理的内聚MODULE,于是现在便有了的很多MODULE。在这种情况下,开发人员要想查找某个功能,应该到哪个MODULE中去查呢?如果有了一个新类,应该把它放在哪里?这些小软件包的实际意义是什么?它们又是如何协同工作的呢?而且以后还要创建更多的MODULE。
开发人员互相之间仍然能够进行很好的沟通,而且也知道每天都要做什么工作,但项目领导者却不满足这种一知半解的状态。他们需要某种组织设计的方式,以便在项目进入到更复杂的阶段时能够理解和掌控它。他们进行了头脑风暴活动,发现了很多潜在的办法。开发人员提出了不同的打包方案。有一些文档给出了系统的全貌,还有一些使用建模工具绘制的类图——新视图可以用来指引开发人员找到正确的模块。但项目领导者对这些小花招并不满意。
他们可以用模型把模拟器的工作流程简单地描述出来,也可以说清楚基础设施是如何序列化数据的,以及电信技术层怎样保证数据的完整性和路由选择。模型中包含了所有细节,却没有一条清楚的主线。
领域的一些重要概念丢失了。但这次丢失的不是对象模型中的一两个类,而是整个模型的结构。
经过一两周的仔细思考之后,开发人员有了思路。他们打算把设计放到一个结构中。整个模拟器将被看作由一系列层组成,这些层分别对应于通信系统的各个方面。最下面的层用来表示物理基础设施,它具有将数据位从一个节点传送到另一个节点的基本能力。它的上面是封包路由层,与数据流定向有关的问题都被集中到这一层中。其他的层则表示其他概念层次的问题。这些层共同描述了系统的大致情况。
他们开始按照新的结构来重构代码。为了不让MODULE跨越多个层,必须对它们重新定义。在一些情况下,还需要重构对象职责,以便明确地让每个对象只属于一个层。另一方面,借由应用这些新思路的实际经验,概念层本身的定义也得到了精化。层、MODULE和对象一起演变,最后,整个设计都符合了这种分层结构的大体轮廓。这些层并不是MODULE,也不是任何其他的代码工件。它们是一种全局性的规则集,用于约束整个设计中的任何MODULE或对象(甚至包括与其他系统的接口)的边界和关系。
实施了这种分层级别之后,设计重新变得易于理解了。人们基本上知道到哪里去寻找某个特定功能。分工不同的开发人员所做的设计决策可以大体上互相保持一致。这样就可以处理更加复杂的设计了。
即使将MODULE分解,一个大模型的复杂性也可能会使它变得很难掌握。MODULE确实把设计分解为更易管理的小部分,但MODULE的数量可能会很多。此外,模块化并不一定能够保证设计的一致性。对象与对象之间,包与包之间,可能应用了一堆的设计决策,每个决策看起来都合情合理,但总的来看却非常怪异。
严格划分BOUNDED CONTEXT可能会防止出现破坏和混淆,但其本身对于从整体上审视系统并无任何助益。
精炼可以帮助我们把注意力集中于CORE DOMAIN,并将SUBDOMAIN分离出来,让它们承担支持性的职责。但我们仍然需要理解这些支持性元素,以及它们与CORE DOMAIN的关系,还有它们互相之间的关系。理想的情况是,整个CORE DOMAIN非常清楚和易于理解,因此不再需要额外的指导,但我们并不总能处于这样好的境况中。
无论项目的规模如何,人们总需要有各自的分工,来负责系统的不同部分。如果没有任何协调机制或规则,那么相同问题的各种不同风格和截然不同的解决方案就会混杂在一起,使人们很难理解各个部分是如何组织在一起的,也不可能看到整个系统的统一视图。从设计的一个部分学到的东西并不适用于这个设计的其他部分,因此项目最后的结果是开发人员成为各自MODULE的专家,一旦脱离了他们自己的小圈子就无法互相帮助。在这种情况下,CONTINUOUS INTEGRATION根本无法实现,而BOUNDED CONTEXT也使项目变得支离破碎。
在一个大的系统中,如果因为缺少一种全局性的原则而使人们无法根据元素在模式(这些模式被应用于整个设计)中的角色来解释这些元素,那么开发人员就会陷入"只见树木,不见森林"的境地。
我们需要理解各个部分在整体中的角色,而不必去深究细节。
"大型结构"是一种语言,人们可以用它来从大局上讨论和理解系统。它用一组高级概念或规则(或两者兼有)来为整个系统的设计建立一种模式。这种组织原则既能指导设计,又能帮助理解设计。另外,它还能够协调不同人员的工作,因为它提供了共享的整体视图,让人们知道各个部分在整体中的角色。
设计一种应用于整个系统的规则(或角色和关系)模式,使人们可以通过它在一定程度上了解各个部分在整体中所处的位置(即使是在不知道各个部分的详细职责的情况下)。
这种结构可以被限制在一个BOUNDED CONTEXT中,但通常情况下它会跨越多个BOUNDED CONTEXT,并通过提供一种概念组织把项目涉及的所有团队和子系统紧密结合到一起。好的结构可以帮助人们深入地理解模型,还能够对精炼起到补充作用。
大部分大型结构都无法用UML来表示,而且也不需要这样做。这些大型结构是用来勾画和解释模型及设计的,但在设计中并不出现,它们只是用来表达设计的另外一种方式。在本章的示例中,你将看到许多添加了大型结构信息的非正式的UML图。
当团队规模较小而且模型也不太复杂时,只需将模型分解为合理命名的MODULE,再进行一定程度的精炼,然后在开发人员之间进行非正式的协调,以上这些就足以使模型保持良好的组织结构了。
大型结构可以节省项目的开发费用,但不适当的结构会严重妨碍开发的进展。本章将探讨一些能成功构建这种设计结构的模式。
模式:EVOLVING ORDER
很多开发人员都亲身经历过由于设计结构混乱而产生的代价。为了避免混乱,项目通过架构从各个方面对开发进行约束。一些技术架构确实能够解决技术问题,如网络或数据持久化问题,但当我们在应用层和领域模型中使用架构时,它们可能会产生自己的问题。它们往往会妨碍开发人员创建适合于解决特定问题的设计和模型。一些要求过高的架构甚至会妨碍编程语言本身的使用,导致应用程序开发人员根本无法使用他们在编程语言中最熟悉的和技术能力很强的一些功能。而且,无论架构是面向技术的,还是面向领域的,如果其限定了很多前期设计决策,那么随着需求的变更和理解的深入,这些架构会变得束手束脚。
近年来,一些技术架构(如J2EE)已经成为主流技术,而人们对领域层中的大型结构却没有做多少研究,这是因为应用程序不同,其各自的需求也大为不同。
在项目前期使用大型结构可能需要很大的成本。随着开发的进行,我们肯定会发现更适当的结构,甚至会发现先前使用的结构妨碍了我们采取一种使应用程序更清晰和简化的路线。这种结构的一部分是有用的,但却使你失去了其他很多机会。你的工作会慢下来,因为你要寻找解决的办法或试着与架构师们进行协商。但经理会认为架构已经定下来了,当初选这个架构就是因为它能够使应用程序变得简单一些,那为什么不去开发应用程序,却在这些架构问题上纠缠不清呢?即使经理和架构团队能够接受这些问题,但如果每次修改都像是一场攻坚战,那么人们很快就会疲乏不堪。
一个没有任何规则的随意设计会产生一些无法理解整体含义且很难维护的系统。但架构中早期的设计假设又会使项目变得束手束脚,而且会极大地限制应用程序中某些特定部分的开发人员/设计人员的能力。很快,开发人员就会为适应结构而不得不在应用程序的开发上委曲求全,要么就是完全推翻架构而又回到没有协调的开发老路上来。问题并不在于指导规则本身应不应该存在,而在于这些规则的严格性和来源。如果这些用于控制设计的规则确实符合开发环境,那么它们不但不会阻碍开发,而且还会推动开发在健康的方向上前进,并且保持开发的一致性。
让这种概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同的结构风格。不要依此过分限制详细的设计和模型决策,这些决策和模型决策必须在掌握了详细知识之后才能确定。
有时个别部分具有一些很自然且有用的组织和表示方式,但这些方式并不适用于整体,因此施加全局规则会使这些部分的设计不够理想。在选择大型结构时,应该侧重于整体模型的管理,而不是优化个别部分的结构。因此,在"结构统一"和"用最自然的方式表示个别组件"之间需要做出一些折中选择。根据实际经验和领域知识来选择结构,并避免采用限制过多的结构,如此可以降低折中的难度。真正适合领域和需求的结构能够使细节的建模和设计变得更容易,因为它快速排除了很多选项。
大型结构还能够为我们做设计决策提供捷径,虽然原则上也可以通过研究各个对象来做出这些决策,但实际上这会耗费太长时间,而且产生的结果可能不一致。当然,持续重构仍然是必要的,但这种结构可以帮助重构变得更易于管理,并使不同的人能够得到一致的解决方案。
大型结构通常需要跨越BOUNDED CONTEXT来使用。在经历了实际项目上的迭代之后,结构将失去与特定模型紧密联系的特性,也会得到符合领域的CONCEPTUAL CONTOUR的特性。这并不意味着它不能对模型做出任何假设,而是说它不会把专门针对局部情况而做的假设强加于整个项目。它应该为那些在不同CONTEXT中工作的开发团队保留一定的自由,允许他们为了满足局部需要而修改模型。
此外,大型结构必须适应开发工作中的实际约束。例如,设计人员可能无法控制系统的某些部分的模型,特别是外部子系统或遗留子系统。这个问题有多种解决方式,如修改结构使之更适应特定外部元素,或者指定应用程序与外部元素的关联方式,或者使结构变得足够松散,以灵活应对难以处理的现实情况。
与CONTEXT MAP不同的是,大型结构是可选的。当使用某种结构可以节省成本并带来益处时,并且发现了一种适当的结构,就应该使用它。实际上,如果一个系统简单到把它分解为MODULE就足以理解它,那么就不必使用这种结构了。当发现一种大型结构可以明显使系统变得更清晰,而又没有对模型开发施加一些不自然的约束时,就应该采用这种结构。使用不合适的结构还不如不使用它,因此最好不要为了追求设计的完整性而勉强去使用一种结构,而应该找到尽可能精简的方式解决所出现问题。要记住宁缺勿滥的原则。
大型结构可能非常有帮助,但也有少数不适用的情况,这些例外情况应该以某种方式被标记出来,以便让开发人员知道在没有特殊注明时可以遵循这种结构。如果不适用的情况开始大量出现,就要修改这种结构了,或者干脆不用它。
如前所述,要想创建一种既为开发人员保留必要自由度同时又能保证开发工作不会陷入混乱的结构绝非易事。尽管人们已经在软件系统的技术架构上投入了大量工作,但有关领域层的结构化研究还很少见。一些方法会破坏面向对象的范式,如那些按应用任务或按用例对领域进行分解的方法。整个领域的研究还很贫瘠。本章将讨论4种模式,其中可能会有一种符合你的需要,或者能够为你提供一些思路,从而找到一种适合你的项目的结构。
模式:SYSTEM METAPHOR
METAPHOR(比喻)的思维在软件开发(特别是模型)中是很普遍的。但极限编程中的"比喻"却具有另外一种含义,它用一种特殊的比喻方式来使整个系统的开发井然有序。
一栋大楼的防火墙能够在周围发生火灾时防止火势从其他建筑蔓延到它自身,同样,软件"防火墙"可以保护局部网络免受来自更大的外部网的破坏。这个"防火墙"的比喻对网络架构产生了很大影响,并且由此而产生了一整套产品类别。有多种互相竞争的防火墙软件可供消费者选择,它们都是独立开发的,而且人们知道它们在一定程度上可以互换。即使网络的初学者也很容易掌握这个概念。这种在整个行业和客户中的共同理解很大一部分上得益于比喻的方式。
然而这个类比却并不准确,而且防火墙从功能上来看也是把双刃剑。防火墙的比喻引导人们开发出了软件屏障,但有时它并不能起到充分的防护作用,而且会阻止正当的数据交换,同时也无法防护来自网络内部的威胁。例如,WLAN就存在这种漏洞。防火墙这个形象的比喻确实很有用,但比喻也都是有弊端的。
软件设计往往非常抽象且难于掌握。开发人员和用户都需要一些切实可行的方式来理解系统,并共享系统的一个整体视图。
从某种程度上讲,比喻对人们的思考方式有着深刻地影响,它已经渗透到每个设计中。系统有很多"层",层与层之间依次叠放起来。系统还有"内核",位于这些层的"中心"。但有时比喻可以传达整个设计的中心主题,并能够在团队所有成员中形成共同理解。
在这种情况下,系统实际上就是由这个比喻塑造的。开发人员所做的设计决策也将与系统比喻保持一致。这种一致性使其他开发人员能够根据同一个比喻来解释复杂系统中的多个部分。开发人员和专家在讨论时有一个比模型本身更具体的参考点。
**SYSTEM METAPHOR(系统比喻)是一种松散的、易于理解的大型结构,它与对象范式是协调的。**由于系统比喻只是对领域的一种类比,因此不同模型可以用近似的方式来与它关联,这使得人们能够在多个BOUNDED CONTEXT中使用系统比喻,从而有助于协调各个BOUNDED CONTEXT之间的工作。
SYSTEM METAPHOR是极限编程的核心实践之一,因此它已经成为一种非常流行的方法(Beck 2000)。遗憾的是,很少有项目能够找到真正有用的METAPHOR,而且人们有时还会把一些起反作用的隐喻思想灌输到领域中。有时使用太强的隐喻反而会有风险,因为它使设计中掺杂了一些与当前问题无关的类比,或者是类比虽然很有吸引力,但它本身并不恰当。
尽管如此,SYSTEM METAPHOR仍然是众所周知的大型结构,它对一些项目非常有用,而且很好地说明了结构的总体概念。
当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向着一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。围绕这个隐喻来组织设计,并把它吸收到UBIQUITOUS LANGUAGE中。SYSTEM METAPHOR应该既能促进系统的交流,又能指导系统的开发。它可以增加系统不同部分之间的一致性,甚至可以跨越不同的BOUNDED CONTEXT。但所有隐喻都不是完全精确的,因此应不断检查隐喻是否过度或不恰当,当发现它起到妨碍作用时,要随时准备放弃它。
SYSTEM METAPHOR并不适用于所有项目。从总体上讲,大型结构并不是必须要用的。在极限编程的12个实践中,SYSTEM METAPHOR的角色可以由UBIQUITOUS LANGUAGE来承担。当项目中发现一种非常合适的SYSTEM METAPHOR或其他大型结构时,应该用它来补充UBIQUITOUS LANGUAGE。
模式:RESPONSIBILITY LAYER
如果每个对象的职责都是人为分配的,将没有统一的指导原则和一致性,也无法把领域作为一个整体来处理。为了保持大模型的一致,有必要在职责分配上实施一定的结构化控制。
当对领域有了深入的理解后,大的模式会变得清晰起来。一些领域具有自然的层次结构。某些概念和活动处在其他元素形成的一个大背景下,而那些元素会因不同原因且以不同频率独立发生变化。如何才能充分利用这种自然结构,使它变得更清晰和有用呢?这种自然的层次结构使我们很容易想到把领域分层,这是最成功的架构设计模式之一([Buschmann et al. 1996]等)。
所谓的层,就是对系统进行划分,每个层的元素都知道或能够使用在它"下面"的那些层的服务,但却不知道它"上面"的层,而且与它上面的层保持独立。当我们把MODULE的依赖性画出来时,图的布局通常是具有依赖性的MODULE出现在它所依赖的模块上面。按照这种方式,可以将各层的顺序梳理出来,最终,低层中的对象在概念上不依赖于高层中的对象。这种自发的分层方式虽然使跟踪依赖性变得更容易,而且有时具有一定的直观意义,但它对模型的理解并没有多大的帮助,也不能指导建模决策。我们需要一种具有更明确目的的分层方式。
在一个具有自然层次结构的模型中,可以围绕主要职责进行概念上的分层,这样可以把分层和职责驱动的设计这两个强有力的原则结合起来使用。
这些职责必须比分配给单个对象的职责广泛得多才行。当设计单独的MODULE和AGGREGATE时,要将其限定在其中一个主要职责上。这种明确的职责分组可以提高模块化系统的可理解性,因为MODULE的职责会变得更易于解释。而高层次的职责与分层的结合为我们提供了一种系统的组织原则。
分层模式有一种变体最适合按职责来分层,我们把这种变体称为RELAXED LAYERED SYSTEM(松散分层系统)[Buschmann et al. 1996, p. 45],如果采用这种分层模式,某一层中的组件可以访问任何比它低的层,而不限于只能访问直接与它相邻的下一层。
注意观察模型中的概念依赖性,以及领域中不同部分的变化频率和变化的原因。如果在领域中发现了自然的层次结构,就把它们转换为宽泛的抽象职责。这些职责应该描述系统的高层目的和设计。对模型进行重构,使得每个领域对象、AGGREGATE和MODULE的职责都清晰地位于一个职责层当中。
选择适当的层
要想找到一种适当的RESPONSIBILITY LAYER或大比例结构,需要理解问题领域并反复进行实验。如果遵循EVOLVING ORDER,那么最初的起点并不是十分重要,尽管差劲的选择确实会加大工作量。结构可能最后演变得面目全非。因此,下面将给出一些指导方针,无论是刚开始选择一种结构,还是对已有结构进行转换,这些指导方针都适用。
当对层进行删除、合并、拆分和重新定义等操作时,应寻找并保留以下一些有用的特征。
场景描述。层应该能够表达出领域的基本现实或优先级。选择一种大比例结构与其说是一种技术决策,不如说是一种业务建模决策。层应该显示出业务的优先级。
概念依赖性。"较高"层概念的意义应该依赖"较低"层,而低层概念的意义应该独立于较高的层。
CONCEPTUAL CONTOUR。如果不同层的对象必须具有不同的变化频率或原因,那么层应该能够容许它们之间的变化。
在为每个新模型定义层时不一定总要从头开始。在一系列相关领域中,有些层是固定的。
模式:KNOWLEDGE LEVEL
当我们需要让用户对模型的一部分有所控制,而模型又必须满足更大的一组规则时,可以利用KNOWLEDGE LEVEL(知识级别)来处理这种情况。它可以使软件具有可配置的行为,其中实体中的角色和关系必须在安装时(甚至在运行时)进行修改。
在《分析模式》[Fowler 1996, pp. 24–27]一书中,KNOWLEDGE LEVEL这种模式是讨论在组织内部对责任进行建模的时候提到的,后来在会计系统的过账规则中也用到了这种模式。虽然有几章内容涉及此模式,但并没有为它单独开一章,因为它与书中所讨论的大部分模式都不相同。KNOWLEDGE LEVEL并不像其他分析模式那样对领域进行建模,而是用来构造模型的。为了使问题更具体,我们来考虑一下"责任"(accountability)模型。组织是由人和一些更小的组织构成的,并且定义了他们所承担的角色和互相之间的关系。不同的组织用于控制这些角色和关系的规则大不相同。有的公司分为各个"部门",每个部门可能由一位"主管"来领导,他要向"副总裁"汇报。而有的公司则分为各个"模块"(module),每个模块由一位"经理"来领导,他要向"高级经理"汇报。还有一些组织采用的是"矩阵"形式,其中每个人都出于不同的目的而向不同的经理汇报。
一般的应用程序都会做一些假设。当这些假设并不恰当时,用户就会在数据录入字段中输入与预期不符的数据。由于语义被用户改变,因此应用程序的任何行为都可能会失败。用户将会想出一些迂回的办法来执行这些行为,或者关闭一些高级特性。他们不得不费力地找出他们的操作与软件行为之间的复杂对应关系。这样他们永远也得不到良好的服务。当必须要对系统进行修改或替换时,开发人员(或迟或早)会发现,有一些功能的真实含义并不像它们看上去的那样。它们在不同的用户社区或不同情况下具有完全不同的含义。在不破坏这些互相叠加的含义的前提下修改任何东西都是非常困难的。要想把数据迁移到一个"更合适"的系统中,必须要理解这些奇怪的部分,并对其进行编码。
静态模型可能引起问题。但在一个过于灵活的系统中,如果任何可能的关系都允许存在,问题一样糟糕。这样的系统使用起来会很不方便,而且会导致组织无法实施自己的规则。
让每个组织完全定制自己的软件也是不现实的,即使组织能够担负得起定制软件的费用,组织结构也可能会频繁变化。
因此,这样的软件必须为用户提供配置选项,以便反映出组织的当前结构。问题是,在模型对象中添加这些选项会使这些对象变得难于处理。要求的灵活性越高,模型就会变得越复杂。
如果在一个应用程序中,ENTITY的角色和它们之间的关系在不同的情况下有很大变化,那么复杂性会显著增加。在这种情况下,无论是一般的模型还是高度定制的模型,都无法满足用户的需求。为了兼顾各种不同的情形,对象需要引用其他的类型,或者需要具备一些在不同情况下包括不同使用方式的属性。具有相同数据和行为的类可能会大量增加,而这些类的唯一作用只是为了满足不同的组装规则。
在我们的模型中嵌入了另一个模型,而它的作用只是描述我们的模型。KNOWLEDGE LEVEL分离了模型的这个自我定义的方面,并清楚地显示了它的限制。
KNOWLEDGE LEVEL是REFLECTION(反射)模式在领域层中的一种应用,很多软件架构和技术基础设施中都使用了它,[Buschmann et al. 1996]中给出了详尽介绍。REFLECTION模式能够使软件具有"自我感知"的特性,并使所选中的结构和行为可以接受调整和修改,从而满足变化需要。
这是通过将软件分为两个层来实现的,一个层是"基础级别"(base level),它承担应用程序的操作职责;另一个是"元级别"(meta level),它表示有关软件结构和行为方面的知识。
值得注意的是,我们并没有把这种模式叫做知识"层"(layer)。虽然REFLECTION与分层很类似,但反射却包含双向依赖关系。
Java有一些最基本的内置REFLECTION机制,它们采用的是协议的形式,用于查询一个类的方法等。这样的机制允许用户查询有关它自己的一些设计信息。CORBA也有一些扩展(但类似)的REFLECTION协议。一些持久化技术增加了更丰富的自描述特性,在数据表与对象之间提供了部分自动化的映射。还有其他一些技术例子。这种模式也可以在领域层中使用。
KNOWLEDGE LEVEL具有两个很有用的特性。首先,它关注的是应用领域,这一点与人们所熟悉的REFLECTION模式的应用正好相反。其次,KNOWLEDGE LEVEL并不追求完全的通用性。正如一个SPECIFICATION可能比通用的断言更有用一样,专门为一组对象和它们的关系定制的一个约束集可能比一个通用的框架更有用。KNOWLEDGE LEVEL显得更简单,而且可以传达设计者的特别意图。
创建一组不同的对象,用它们来描述和约束基本模型的结构和行为。把这些对象分为两个"级别",一个是非常具体的级别,另一个级别则提供了一些可供用户或超级用户定制的规则和知识。
像所有有用的思想一样,REFLECTION和KNOWLEDGE LEVEL可能令人们感到振奋,但不应滥用这种模式。它确实能够使对象不必为了满足各种不同情形下的需求而变得过于复杂,但它所引入的间接性也会使系统变得更模糊。如果KNOWLEDGE LEVEL太复杂,开发人员和用户就很难理解系统的行为。负责配置它的用户(或超级用户)最终将需要具备程序员的技能,甚至需要掌握处理元数据的技能。如果他们出现了错误,应用程序也将会产生错误行为。
而且,数据迁移的基本问题并没有完全得到解决。当KNOWLEDGE LEVEL中的某个结构发生变化时,必须对现有的操作级别中的对象进行相应的处理。新旧对象确实可以共存,但无论如何都需要进行仔细的分析。
所有这些问题为KNOWLEDGE LEVEL的设计人员增加了一个沉重的负担。设计必须足够健壮,因为不仅要解决开发中可能出现的各种问题,而且还要考虑到将来用户在配置软件时可能会出现的各种问题。如果得到合理的运用,KNOWLEDGE LEVEL能够解决一些其他方式很难解决的问题。如果系统中某些部分的定制非常关键,而要是不提供定制能力就会破坏掉整个设计,这时就可以利用KNOWLEDGE LEVEL来解决这一问题。
乍看上去,KNOWLEDGE LEVEL像是RESPONSIBILITY LAYER的一个特例,但它并不是。首先,两个级别之间的依赖性是双向的,而在层次结构中,较低的层不依赖于较高的层。实际上,RESPONSIBILITY LAYER可以与其他大部分的大比例结构共存,它提供了另一种用来组织模型的维度。
模式:PLUGGABLE COMPONENT FRAMEWORK
在深入理解和反复精炼基础上得到的成熟模型中,会出现很多机会。通常只有在同一个领域中实现了多个应用程序之后,才有机会使用PLUGGABLE COMPONENT FRAMEWORK(可插入式组件框架)。
当很多应用程序需要进行互操作时,如果所有应用程序都基于相同的一些抽象,但它们是独立设计的,那么在多个BOUNDED CONTEXT之间的转换会限制它们的集成。各个团队之间如果不能紧密地协作,就无法形成一个SHARED KERNEL。重复和分裂将会增加开发和安装的成本,而且互操作会变得很难实现。
一些成功的项目将它们的设计分解为组件,每个组件负责提供某些类别的功能。通常所有组件都插入到一个中央hub上,这个hub支持组件所需的所有协议,并且知道如何与它们所提供的接口进行对话。还有其他一些将组件连在一起的可行模式。对这些接口以及用于连接它们的hub的设计必须要协调,而组件内部的设计则可以更独立一些。
有几个广泛使用的技术框架支持这种模式,但这只是次要问题。一种技术框架只有在能够解决某类重要技术问题的时候才有必要使用,如在设计分布式系统或在不同应用程序中共享一个组件时。可插入式组件框架的基本模式是职责的概念组织,它很容易在单个的Java程序中使用。
从接口和交互中提炼出一个ABSTRACT CORE,并创建一个框架,这个框架要允许这些接口的各种不同实现被自由替换。同样,无论是什么应用程序,只要它严格地通过ABSTRACT CORE的接口进行操作,那么就可以允许它使用这些组件。
高层抽象被识别出来,并在整个系统范围内共享,而特化(specialization)发生在MODULE中。应用程序的中央hub是SHARED KERNEL内部的ABSTRACT CORE。但封装的组件接口可以把多个BOUNDED CONTEXT封装到其中,这样,当很多组件来自多个不同地方时,或者当组件中封装了用于集成的已有软件时,可以很方便地使用这种结构。
这并不是说不同组件一定要使用不同的模型。只要团队采用了CONTINUOUS INTEGRATE,或者为一组密切相关的组件定义了另一个SHARED KERNEL,那么就可以在同一个CONTEXT中开发多个组件。在PLUGGABLE COMPONENT FRAMEWORK这种大比例结构中,所有这些策略很容易共存。在某些情况下,还有一种选择是使用一种PUBLISHED LANGUAGE来编写hub的插入接口。
PLUGGABLE COMPONENT FRAMEWORK也有几个缺点。一个缺点是它是一种非常难以使用的模式。它需要高精度的接口设计和一个非常深入的模型,以便把一些必要的行为捕获到ABSTRACT CORE中。另一个很大的缺点是它只为应用程序提供了有限的选择。如果一个应用程序需要对CORE DOMAIN使用一种非常不同的方法,那么可插入式组件框架将起到妨碍作用。开发人员可以对模型进行特殊修改,但如果不更改所有不同组件的协议,就无法修改ABSTRACT CORE。这样一来,CORE的持续精化过程(也是通过重构得到更深层理解的过程)在某种程度上会陷入僵局。
[Fayad and Johnson 2000]中详细介绍了在几个领域中使用PLUGGABLE COMPONENT FRAMEWORK的大胆尝试,其中包括对SEMATECH CIM框架的讨论。要想成功地使用这些框架,需要综合考虑很多事情。最大的障碍可能就是人们的理解不那么成熟,要想设计一个有用的框架,必须要有成熟的理解。PLUGGABLE COMPONENT FRAMEWORK不适合作为项目的第一个大比例结构,也不适合作为第二个。最成功的例子都是在完全开发出了多个专门应用之后才采用这种结构的。
结构应该有一种什么样的约束
本章所讨论的大比例结构很广泛,从非常宽松的SYSTEM METAPHOR到严格的PLUGGABLE COMPONENT FRAMEWORK。当然,还有很多其他结构,而且,甚至在一个通用的结构模式中,在制定规则上也可以选择多种不同的严格程度。
例如,RESPONSIBILITY LAYER规定了一种用于划分模型概念以及它们的依赖性的方式,但我们也可以添加一些规则,来指定各个层之间的通信模式。
通过重构得到更适当的结构
在当今这个时代,软件开发行业正在努力摆脱过多的预先设计,因此一些人会把大比例结构看作是倒退回了过去那段使用瀑布架构的令人痛苦的年代。但实际上,只有深入地理解领域和问题才能发现一种非常有用的结构,而获得这种深刻的理解的有效方式就是迭代开发过程。
团队要想坚持EVOLVING ORDER原则,必须在项目的整个生命周期中大胆地反复思考大比例结构。团队不应该一成不变地使用早期构思出来的那个结构,因为那时所有人对领域或需求的理解都不够完善。
遗憾的是,这种演变意味着最终的结构不会在项目一开始就被发现,而且我们必须在开发过程中进行重构,以便得到最终的结构。这可能很难实现,而且需要高昂的代价,但这样做是非常必要的。有一些通用的方法可以帮助控制成本并最大化收益。
最小化
控制成本的一个关键是保持一种简单、轻量级的结构。不要试图使结构面面俱到。只需解决最主要的问题即可,其他问题可以留到后面一个一个地解决。
开始最好选择一种松散的结构,如SYSTEM METAPHOR或几个RESPONSIBILITY LAYER。不管怎样,一种最小化的松散结构可以起到轻量级的指导作用,它有助于避免混乱。
沟通和自律
整个团队在新的开发和重构中必须遵守结构。要做到这一点,整个团队必须理解这种结构。必须把术语和关系纳入到UBIQUITOUS LANGUAGE中。
大比例结构为项目提供了一个术语表,它概要地描述了整个系统,并且使不同人员能够做出一致的决策。但由于大多数大比例结构只是松散的概念指导,因此团队必须要自觉地遵守它。
如果很多人不遵守结构,它慢慢就会失去作用。这时,结构与模型和实现的各个部分之间的关系无法总是在代码中明确地反映出来,而且功能测试也不再依赖结构了。此外,结构往往是抽象的,因此很难保证在一个大的团队(或多个团队)中一致地应用它。
在大多数团队中,仅仅通过沟通是不足以保证在系统中采用一致的大比例结构的。至关重要的一点是要把它合并到项目的通用语言中,并让每个人都严格地使用UBIQUITOUS LANGUAGE。
通过重构得到柔性设计
其次,对结构的任何修改都可能导致大量的重构工作出现。随着系统复杂度的增加和人们理解的加深,结构会不断演变。每次修改结构时,必须修改整个系统,以便遵守新的秩序。显然这需要付出大量工作。
但这并不像听上去那么糟糕。采用了大比例结构的设计往往比那些未采用的设计更容易转换。即使是从一种结构更改为另一种结构(例如,从METAPHOR改为LAYER)也是如此。部分原因是当完全理解了某个系统的当前布局之后,再重新安排它就会更容易,而且先前的结构使得重新布局变得更容易。还有部分原因是用于维护先前结构的那种自律性已经渗透到了系统的各个方面。但还有更多的原因,因为当一个系统先前已经使用了两种结构时,它的更改甚至更加容易。
一件新皮茄克穿起来又硬又不舒服,但穿了一天之后,肘部经过若干次弯曲后就会变得更容易弯曲。再穿几天之后,肩部也会变得宽松,茄克也更容易穿上了。几个月后,皮质开始变得柔软,穿着会更舒适,也更容易穿上。同样,对模型反复进行合理的转换也有相同效果。不断增加的知识被合并到模型中,更改的要点已经被识别出来,并且更改也变得更灵活,同时模型中一些稳定的部分也得到了简化。这样,底层领域的更显著的CONCEPTUAL CONTOUR就会在模型结构中浮现出来。
通过精炼可以减轻负担
对模型施加的另一项关键工作是持续精炼。这可以从各个方面减小修改结构的难度。首先,从CORE DOMAIN中去掉一些机制、GENERIC SUBDOMAIN和其他支持结构,需要重构的内容就少多了。
如果可能的话,应该把这些支持元素简单地定义成符合大比例结构的形式。例如,在一个RESPONSIBILITY LAYER系统中,可以把GENERIC SUBDOMAIN定义成只适合放到某个特定层中。当使用了PLUGGABLE COMPONENT FRAMEWORK的时候,可以把GENERIC SUBDOMAIN定义成完全由某个组件拥有,也可以定义成一个SHARED KERNEL,供一组相关组件使用。这些支持元素可能需要进行重构,以便找到它们在结构中的适当位置,但它们的移动与CORE DOMAIN是独立的,而且移动也限制在很小的范围内,因此更容易实现。最后,它们都是次要元素,因此它们的精化不会影响大局。
通过精炼和重构得到更深层理解的原理甚至也适用于大比例结构本身。例如,最初可以根据对领域的初步理解来选择分层结构,然后逐步用更深层次的抽象(这些抽象表达了系统的基本职责)来代替它们。这种极高的清晰度使人们能够透彻地理解领域,这也是我们的目标。它也是一种使系统的整体控制变得更容易、更安全的手段。
参考
《领域驱动设计 软件核心复杂性应对之道》 Eric Evans 著, 赵俐 盛海艳 刘霞 等译, 任发科 审校