第15章 精炼

第15章 精炼 - 图1

——James Clerk Maxwell,A Treatise on Electricity and Magnetism,1873

上面这4个方程式,再加上其中的术语定义,以及它们所依赖的数学体系,表达了19世纪经典电磁学的全部内涵。

如何才能专注于核心问题而不被大量的次要问题淹没呢?LAYERED ARCHITECTURE可以把领域概念从技术逻辑中(技术逻辑确保了计算机系统能够运转)分离出来,但在大型系统中,即使领域被分离出来,它的复杂性也可能仍然难以管理。

精炼是把一堆混杂在一起的组件分开的过程,以便通过某种形式从中提取出最重要的内容,而这种形式将使它更有价值,也更有用。模型就是知识的精炼。通过每次重构所得到的更深层的理解,我们得以把关键的领域知识和优先级提取出来。现在,让我们回过头来从战略角度看一下精炼,本章将介绍对模型进行粗线条划分的各种方式,并把领域模型作为一个整体进行精炼。

像很多化学蒸馏过程一样,精炼过程所分离出来的副产品(如GENERIC SUBDOMAIN和COHERENT MECHANISM)本身也很有价值,但精炼的主要动机是把最有价值的那部分提取出来,正是这个部分使我们的软件区别于其他软件并让整个软件的构建物有所值,这个部分就是CORE DOMAIN。

领域模型的战略精炼包括以下部分:

(1) 帮助所有团队成员掌握系统的总体设计以及各部分如何协调工作;

(2) 找到一个具有适度规模的核心模型并把它添加到通用语言中,从而促进沟通;

(3) 指导重构;

(4) 专注于模型中最有价值的那部分;

(5) 指导外包、现成组件的使用以及任务委派。

本章将展示对CORE DOMAIN进行战略精炼的系统性方法,解释如何在团队中有效地统一认识,并提供一种用于讨论工作的语言。

第15章 精炼 - 图2 图15-1 战略精炼的导航图

像那些园丁为了让树干快速生长而修剪树苗一样,我们将使用一整套技术把模型中那些细枝末节砍掉,从而把注意力集中在最重要的部分上……

15.1 模式:CORE DOMAIN

第15章 精炼 - 图3

在设计大型系统时,有非常多的组成部分——它们都很复杂而且对开发的成功也至关重要,但这导致真正的业务资产——领域模型最为精华的部分——被掩盖和忽略了。

难以理解的系统修改起来会很困难,而且修改的结果也难以预料。开发人员如果脱离自己熟悉的领域,也会迷失方向(当团队中有新人加入时尤其如此,但老成员也面临同样的状况,除非代码表达得非常清楚并且组织有序)。这样一来就必须分门别类地为人们安排任务。当开发人员把他们的工作限定到具体的模块时,知识的传递就更少了。这种工作上的划分导致系统很难平滑地集成,也无法灵活地分配工作。如果开发人员没有了解到某项功能已经被实现了,那么就会出现重复,这样系统会变得更加复杂。

以上只是难以理解的设计所导致的一部分后果。当失去了领域的整体视图时,还存在另一个同样严重的风险。

一个严峻的现实是我们不可能对所有设计部分进行同等的精化,而是必须分出优先级。为了使领域模型成为有价值的资产,必须整齐地梳理出模型的真正核心,并完全根据这个核心来创建应用程序的功能。但本来就稀缺的高水平开发人员往往会把工作重点放在技术基础设施上,或者只是去解决那些不需要专门领域知识就能理解的领域问题(这些问题都已经有了很好的定义)。

计算机科学家对系统的这些部分更感兴趣,他们认为通过这些工作可以让自己具备一些在其他地方也能派上用场的专业技能,同时也丰富了个人简历。而真正体现应用程序价值并且使之成为业务资产的领域核心却通常是由那些技术水平稍差的开发人员完成的,他们与DBA一起创建数据模式,然后逐个特性编写代码,而根本没有对模型的概念能力加以任何利用。

如果软件的这个部分实现得很差,那么无论技术基础设施有多好,无论支持功能有多完善,应用程序永远都不会为用户提供真正有吸引力的功能。这个严重问题的根源在于项目没有一个明确的整体设计视图,而且也没有认清各个部分的相对重要性。

我曾经参与过的最成功的项目中,有一个开始时就受到了这种问题的困扰。这个项目的目标是开发一个非常复杂的联合贷款系统。技术能力最强的开发人员在数据库映射层和消息传递接口这些工作上忙得不亦乐乎,而业务模型则交到了那些不熟悉对象技术的新人手中。

唯一的例外是有一个领域问题是由一位经验丰富的对象开发人员处理的,他为那些长期存在的领域对象设计了一种添加注释的功能。通过把这些注释组织到一起,交易商能够看到他们或其他人过去所做的一些决策的基本思想。这位开发人员还构建了一个优秀的用户界面,它为用户提供了直观的访问,用户可以利用这个界面来灵活地使用注释模型的各种功能。

这些特性很有用处,而且设计得很好。它们被合并到最终产品中。

遗憾的是,它们只是一些次要特性。这位能力超群的开发人员把一种有趣的、通用的注释方法建模出来,并干净利落地实现了它,最后交付到用户手中。同时,另一位能力上不太胜任的开发人员却把关键的“贷款”模块弄得一团糟,项目差一点就因此失败。

在制定项目规划的时候,必须把资源分配给模型和设计中最关键的部分。要想达到这个目的,在规划和开发期间每个人都必须识别和理解这些关键部分。

这些部分是应用程序的标志性部分,也是目标应用程序的核心诉求,它们构成了CORE DOMAIN。CORE DOMAIN是系统中最有价值的部分。

因此:

对模型进行提炼。找到CORE DOMAIN并提供一种易于区分的方法把它与那些起辅助作用的模型和代码分开。最有价值和最专业的概念要轮廓分明。尽量压缩CORE DOMAIN。

让最有才能的人来开发CORE DOMAIN,并据此要求进行相应的招聘。在CORE DOMAIN中努力开发能够确保实现系统蓝图的深层模型和柔性设计。仔细判断任何其他部分的投入,看它是否能够支持这个提炼出来的CORE。

提炼CORE DOMAIN并不容易,但它确实会让一些决策变得容易。你需要投入大量的工作使你的CORE鲜明突出,而其他设计部分则只需依照常规做得实用即可。如果某个设计部分需要保密以便保持竞争优势,那么它就是你的CORE DOMAIN。其他的部分则没有必要隐藏起来。当必须在两个看起来都很有用的重构之间进行抉择时(由于时限的缘故),应该首选对CORE DOMAIN影响最大的那个重构。

本章中的模式能够使我们更容易发现、使用和修改CORE DOMAIN。

15.1.1 选择核心

我们需要关注的是那些能够表示业务领域并解决业务问题的模型部分。

对CORE DOMAIN的选择取决于看问题的角度。例如,很多应用程序需要一个通用的货币模型,用来表示各种货币以及它们的汇率和兑换。另一方面,一个用来支持货币交易的应用程序可能需要更精细的货币模型,这个模型有可能就是CORE的一部分。即使在这种情况下,货币模型中可能有一部分仍是非常通用的。随着对领域理解的不断加深,精炼过程可以持续进行,这会把通用的货币概念分离出来,而只把模型中那些专有的部分保留在CORE DOMAIN中。

在运输应用程序中,CORE可能是以下几方面的模型:货物是如何装船运输的,当集装箱转交时责任是如何转接的,或者特定的集装箱是如何经由不同的运输路线最后到达目的地的。在投资银行中,CORE可能包括委托人和参与者之间的合资模型。

一个应用程序的CORE DOMAIN在另一个应用程序中可能只是通用的支持组件。尽管如此,仍然可以在一个项目中(而且通常在一个公司中)定义一个一致的CORE。像其他设计部分一样,人们对CORE DOMAIN的认识也会随着迭代而发展。开始时,一些特定关系可能显得不重要。而最初被认为是核心的对象可能逐渐被证明只是起支持作用。

下面几节(特别是GENERIC SUBDOMAIN这节)将给出制定这些决策的指导。

15.1.2 工作的分配

在项目团队中,技术能力最强的人员往往缺乏丰富的领域知识。这限制了他们的作用,并且更倾向于分派他们来开发一些支持组件,从而形成了一个恶性循环——知识的缺乏使他们远离了那些能够学到领域知识的工作。

打破这种恶性循环是很重要的,方法是建立一支由开发人员和一位或多位领域专家组成的联合团队,其中开发人员必须能力很强、能够长期稳定地工作并且对学习领域知识非常感兴趣,而领域专家则要掌握深厚的业务知识。如果你认真对待领域设计,那么它就是一项有趣且充满技术挑战的工作。你肯定也会找到持这种观点的开发人员。

从外界聘请一些短期的专业人员来设计CORE DOMAIN的关键环节通常是行不通的,因为团队需要积累领域知识,而且短期人员会造成知识流失。相反,充当培训和指导角色的专家可能非常有价值,因为他们帮助团队建立领域设计技巧,并促进团队成员使用尚未掌握的高级设计原则。

出于类似的原因,购买CORE DOMAIN也是行不通的。人们已经在建立特定于行业的模型框架方面付出了一些工作,著名的例子就是半导体行业协会SEMATECH创立的用于半导体制造自动化的CIM框架,以及IBM为很多业务开发的San Francisco框架。虽然这是一个有吸引力的想法,但除了能够促进数据交换的PUBLISHED LANGUAGE(参见第14章)以外,其他结果并不理想。Domain-Specific Application Frameworks[Fayad and Johnson 2000]一书介绍了这项工作的总体状况。随着这个领域的进步,可能会出现一些更有用的框架。

除了上述原因之外,还有一个更重要的原因需要引起我们的注意。自主开发的软件的最大价值来自于对CORE DOMAIN的完全控制。一个设计良好的框架可能会提供满足你的专门使用需求的高水平抽象,它可以节省开发那些更通用部分的时间,并使你能够专注于CORE。但是,如果它对你的约束超出了这个限度,可能有以下3种原因。

(1) 你正在失去一项重要的软件资产。此时应该让这些限制性的框架退出你的CORE DOMAIN。

(2) 框架所处理的部分并不是你所认为的核心。此时应该重新划定CORE DOMAIN的边界,把你的模型中真正的标志性部分识别出来。

(3) 你的CORE DOMAIN并没有特殊的需求。此时应该考虑采用一种风险更低的解决方案,如购买软件并与你的应用程序进行集成。

不管是哪种情况,创建与众不同的软件还是会回到原来的轨道上——需要一支稳定工作的团队,他们不断积累和消化专业知识,并将这些知识转化为一个丰富的模型。没有捷径,也没有魔法。

15.2 精炼的逐步提升

本章接下来将要介绍各种精炼技术,它们在使用顺序上基本没什么要求,但对设计的改动却大不相同。

一份简单的DOMAIN VISION STATEMENT(领域愿景说明)只需很少的投入,它传达了基本概念以及它们的价值。HIGHLIGHTED CORE(突出核心)可以增进沟通,并指导决策制定,这也只需对设计进行很少的改动甚至无需改动。

更积极的精炼方法是通过重构和重新打包显式地分离出GENERIC SUBDOMAIN,然后单独进行处理。在使用COHESIVE MECHANISM的同时,也要保持设计的通用性、易懂性和柔性,这两个方面可以结合起来。只有除去了这些细枝末节,才能把CORE剥离出来。

重新打包出一个SEGREGATED CORE(分离的核心),可以使这个CORE清晰可见(即使在代码中也是如此),并且促进将来在CORE模型上的工作。

最富雄心的精炼是ABSTRACT CORE(抽象内核),它用纯粹的形式表示了最基本的概念和关系(因此,需要对模型进行全面的重新组织和重构)。

每种技术都需要我们连续不断地投入越来越多的工作,但刀磨得越薄,就会越锋利。领域模型的连续精炼将为我们创造一项资产,使项目进行得更快、更敏捷、更精确。

首先,我们可以把模型中最普通的那些部分分离出去,它们就是GENERIC SUBDOMAIN(通用子领域)。GENERIC SUBDOMAIN与CORE DOMAIN形成鲜明的对比,使我们可以更清楚地理解它们各自的含义。

15.3 模式:GENERIC SUBDOMAIN

模型中有些部分除了增加复杂性以外并没有捕捉或传递任何专门的知识。任何外来因素都会使CORE DOMAIN愈发的难以分辨和理解。模型中充斥着大量众所周知的一般原则,或者是专门的细节,这些细节并不是我们的主要关注点,而只是起到支持作用。然而,无论它们是多么通用的元素,它们对实现系统功能和充分表达模型都是极为重要的。

模型中有你想当然的部分。不可否认,它们确实是领域模型的一部分,但它们抽象出来的概念是很多业务都需要的。比如,各个行业(如运输业、银行业或制造业)都需要某种形式的企业组织图。再比如,很多应用程序都需要跟踪应收账款、开支分类账和其他财务事项,而这些都可以用一个通用的会计模型来处理。

通常,人们投注了大量精力去处理领域的周边问题。我亲眼目睹过两个不同项目都分派了最好的开发人员来重新设计带有时区的日期和时间功能,这些工作耗费了他们数周的时间。虽然这样的组件必须正常工作,但它们并不是系统的概念核心。

即使这样的通用模型元素确实非常重要,整个领域模型仍然需要把系统中最有价值和最特别的方面突出出来,而且整个模型的组织应该尽可能把重点放在这个部分上。当核心与所有相关的因素混杂在一起时,这一点会更难做到。

因此:

识别出那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并放到单独的MODULE中。任何专有的东西都不应放在这些模块中。

把它们分离出来以后,在继续开发的过程中,它们的优先级应低于CORE DOMAIN的优先级,并且不要分派核心开发人员来完成这些任务(因为他们很少能够从这些任务中获得领域知识)。此外,还可以考虑为这些GENERIC SUBDOMAIN使用现成的解决方案或“公开发布的模型”(PUBLISHED MODEL)。

当开发这样的软件包时,有以下几种选择。

选择1:现成的解决方案

有时可以购买一个已实现好的解决方案,或使用开源代码。

优点

可以减少代码的开发。

维护负担转移到了外部。

代码已经在很多地方使用过,可能较为成熟,因此比自己开发的代码更可靠和完备。

缺点

在使用之前,仍需要花时间来评估和理解它。

就业内目前的质量控制水平而言,无法保证它的正确性和稳定性。

它可能设计得过于细致了(远远超出了你的目的),集成的工作量可能比开发一个最小化的内部实现更大。

外部元素的集成常常不顺利。它可能有一个与你的项目完全不同的BOUNDED CONTEXT。

即使不是这样,它也很难顺利地引用你的其他软件包中的ENTITY。

它可能会引入对平台、编译器版本的依赖等。

现成的子领域解决方案是值得我们去考虑的,但如果它们常常会带来麻烦,那么往往就得不偿失了。我曾经看到过一些成功案例——一些应用程序需要非常精细的工作流,它们通过API挂钩(API hook)成功地使用了商用的外部工作流系统。我曾经还见过错误日志被深入地集成到应用程序中。有时,GENERIC SUBDOMAIN被打包为框架的形式,它实现了非常抽象的模型,从而可以与你的应用程序集成来满足你的特殊需求。子组件越通用,其自己的模型的精炼程度越高,它的用处可能就越大。

选择2:公开发布的设计或模型

优点

比自己开发的模型更为成熟,并且反映了很多人的深层知识。

提供了随时可用的高质量文档。

缺点

可能不是很符合你的需要,或者设计得过于细致了(远远超出了你的需要)。

Tom Lehrer(20世纪50和60年代的喜剧作曲家)曾经讲过数学上的成功秘诀是:“抄袭!抄袭。不要让任何人的工作逃过你的眼睛……但一定要把这叫做研究。”在领域建模中,特别是在攻克GENERIC SUBDOMAIN时,这是金玉良言。

当有一个被广泛使用的模型时,如《分析模式》[Fowler 1996]一书中所列举的那些模型(参见第11章),这种方法最为有效。

如果领域中已经有了一种非常正式且严格的模型,那么就使用它。会计和物理学是我们立即能想到的两个例子。这些模型不仅精简和健壮,而且被人们广泛理解,因此可以减轻目前和将来的培训负担(参见10.9.2节)。

如果在一个公开发布的模式中能够发现一个简化的子集,它本身是一致的而且能够满足你的要求,那么就不要强迫自己完全实现一个这样的模型。如果一个模型已经有人很好地研究过了,并且提供了完备的文档,甚至已经得到正规化,那么重新去设计它就没有意义了。

选择3:把实现外包出去

优点

使核心团队可以脱身去处理CORE DOMAIN,那才是最需要知识和经验积累的部分。

开发工作的增加不会使团队规模无限扩大下去,同时又不会导致CORE DOMAIN知识的分散。

强制团队采用面向接口的设计,并且有助于保持子领域的通用性,因为规格已经被传递到外部。

缺点

仍需要核心团队花费一些时间,因为他们需要与外包人员商量接口、编码标准和其他重要方面。

当把代码移交回团队时,团队需要耗费大量精力来理解这些代码。(但是这个开销比理解专用子领域要小一些,因为通用子领域不需要理解专门的背景知识。)

代码质量或高或低,这取决于两个团队能力的高低。

自动测试在外包中可能起到重要作用。应该要求外包人员为他们交付的代码提供单元测试。真正有用的方法是为外包的组件详细说明甚至是编写自动验收测试,这有助于确保质量、明确规格并且使这些组件的再集成变得顺利。此外,“把实现外包出去”能够与“公开发布的设计或模型”完美地组合到一起。

选择4:内部实现

优点

易于集成。

只开发自己需要的,不做多余的工作。

可以临时把工作分包出去。

缺点

需要承受后续的维护和培训负担。

很容易低估开发这些软件包所需的时间和成本。

当然,这也可以与“公开发布的设计或模型”结合起来使用。

GENERIC SUBDOMAIN是你充分利用外部设计专家的地方,因为这些专家不需要深入理解你特有的CORE DOMAIN,而且他们也没有太大的机会学习这个领域。机密性问题可以不用过多关注,因为这些模块几乎不涉及专有信息或业务实践。GENERIC SUBDOMAIN可以减轻对那些不了解领域知识的人员进行培训而带来的负担。

我相信,随着时间的推移,CORE模型的范围将会不断变窄,而越来越多的通用模型将作为框架被实现出来,或者至少被实现为公开发布的模型或分析模式。但是现在,大部分模型仍然需要我们自己开发,但把它们与CORE DOMAIN模型区分开是很有价值的。

示例 两个与时区有关的故事

我曾经两次亲眼目睹项目中最好的开发人员花费好几周的时间来解决各个时区的时间存储和转换问题。虽然我对这样的工作安排总是持怀疑态度,但有时它是必要的,而且下面这两个项目几乎形成了鲜明的对比。

第一个项目是为货物运输系统设计日程安排软件。为了安排国际运输,准确的时间计算是非常必要的,而由于所有这些日程安排都是按照当地时间计算的,因此运输过程的安排必然需要进行时间转换。

既然这项功能需求已经确定了,团队就开始了CORE DOMAIN的开发并利用现有的时间类和一些哑数据进行了一些早期的应用程序迭代。随着应用程序不断成熟,现有的时间类已无法满足项目的要求,而且由于很多国家的时间是不同的,再加上国际日期变更线的复杂性,这个问题变得异常复杂。此时,他们的需求更加明确了,他们开始寻找现成的解决方案,但却没有找到。这样,除了自己构建之外已经别无选择了。

这项任务需要做一番研究并进行精确的设计,因此团队领导打算分派一位最好的程序员来完成它。但这项任务并不需要任何运输方面的专业知识,而且做这项任务也不会获得这样的知识,因此他们选择了一位临时在项目上工作的程序员。

这位程序员并没有从头开始工作。他研究了几个现有的时区实现,但大部分并不能满足需要,于是他决定把BSD Unix的一个公共的解决方案改造一下,它已经有了一个完善的数据库和C语言实现。他通过逆向工程找出了其中的逻辑,并编写了一个数据库导入例程。

事实证明问题比他预计的要难得多(如涉及特殊情况的数据库导入),尽管如此他仍然完成了代码的编写并与CORE进行了集成,最终完成了产品的交付。

在另一个项目上发生的事情就完全不同了。一家保险公司开发一个新的理赔处理系统,他们打算把各种事件发生的时间记录下来(发生车祸的时间、下冰雹的时间等)。这些数据是按照当地时间记录的,因此需要用到时区功能。

当我参加这个项目时,他们已经安排了一位初级(但很聪明的)开发人员来从事这项任务,尽管应用程序的准确需求仍在变化中,而且项目甚至还没有开始尝试第一次迭代。他已经开始尽职尽责地基于假设来构建一个时区模型。

由于不知道需要什么样的功能,这位开发人员假设时区组件应该足够灵活,以便处理任何可能的情况。这个问题对他来说太难了,因此项目又分派了一位高级开发人员来帮助他。他们编写了复杂的代码,但由于还没有具体的应用程序使用这些代码,因此他们根本不知道代码是否能正确工作。

项目由于种种原因而搁浅,时区代码从未派上用场。但如果项目不中断,那么简单地存储标明时区的当地时间可能就足够了,甚至不需要转换,因为这些时间数据主要用作参考,而不是用于计算。即使需要转换,由于所有数据都来自北美洲,时区转换也相对很简单。

过分关注时区带来的主要代价是忽略了CORE DOMAIN模型。如果他们能够把同样的精力放在核心模型上,可能早就为自己的应用程序开发出了一个有效的原型和一个初步的、可以工作的领域模型。此外,那些长期稳定地在项目上工作的开发人员此时本来应该对保险领域有所了解了,以便为团队积累关键知识。

有一件事情这两个团队都做得很正确,那就是把通用的时区模型明确地从CORE DOMAIN中分离出来。如果在运输模型或保险模型中使用各自专用的时区MODULE,那么这会导致核心模型与这个通用的支持模型耦合在一起,使得CORE模型更难以理解(因为它将包含无关的时区细节)。而且时区模块可能更难维护(因为维护人员必须理解核心以及它与时区的相互关系)。

运输项目的策略 保险项目的策略 第15章 精炼 - 图4

技术人员喜欢处理那些可定义的问题(如时区转换),而且很容易就能证明他们花时间做这些工作是值得的。但严格地从优先级角度来看,他们应该先去完成CORE DOMAIN的工作。

15.3.1 通用不等于可重用

注意,虽然我一直在强调这些子领域的通用性,但我并没有提代码的可重用性。现成的解决方案可能适用于某种特殊情况,也可能不适用,但假设你要自己实现代码(内部实现或外包出去),那么不要特别关注代码的可重用性。因为那样做会违反精炼的基本动机——我们应该尽可能把大部分精力投入到CORE DOMAIN工作中,而只在必要的时候才在支持性的GENERIC SUBDOMAIN中投入工作。

重用确实会发生,但不一定总是代码重用。模型重用通常是更高级的重用,例如,当使用公开发布的设计或模型的时候就是如此。如果你必须创建自己的模型,那么它在以后的相关项目中可能很有价值。但是,虽然这样的模型概念可能适用于很多情况,我们也不必把它开发成“万能的”模型。我们只要把业务所需的那部分建模出来并实现即可。

尽管我们很少需要考虑设计的可重用性,但通用子领域的设计必须严格地限定在通用概念的范围之内。如果把行业专用的模型元素引入到通用子领域中,会产生两个后果。第一,它会妨碍将来的开发。虽然现在我们只需要子领域模型的一小部分,但我们的需求会不断增加。如果把任何不属于子领域概念的部分引入到设计中,那么再想灵活地扩展系统就很难了,除非完全重建原来的部分并重新设计使用该部分的其他模块。

第二,也是更重要的,这些行业专用的概念要么属于CORE DOMAIN,要么属于它们自己的更专业的子领域,而且这些专业的模型比通用子领域更有价值。

15.3.2 项目风险管理

敏捷过程通常要求通过尽早解决最具风险的任务来管理风险。特别是XP过程,它要求迅速建立并运行一个端到端的系统。这种初步的系统通常用来检验某种技术架构,而且人们会试图建立一个外围系统,用来处理一些支持性的GENERIC SUBDOMAIN,因为这些子领域通常更易于分析。但是要注意,这可能会不利于风险管理。

项目面临着两方面的风险,有些项目的技术风险更大,有些项目则是领域建模的风险更大一些。端到端的系统是实际系统中最困难部分的“雏形”——它控制风险的能力也仅限于此。当使用这种雏形时,我们很容易低估领域建模的风险。这种风险包括未预料到存在复杂性、与业务专家的交流不够充分,或者开发人员的关键技能存在欠缺等。

因此,除非团队拥有精湛的技术并且对领域非常熟悉,否则第一个雏形系统应该以CORE DOMAIN的某个部分作为基础,不管它有多么简单。

相同的原则也适用于任何试图把高风险的任务放到前面处理的过程。CORE DOMAIN就是高风险的,因为它的难度往往会超出我们的预料,而且如果没有它,项目就不可能获得成功。

本章介绍的大多数精炼模式都展示了如何修改模型和代码,以便提炼出CORE DOMAIN。但是,接下来的两个模式DOMAIN VISION STATEMENT和HIGHLIGHTED CORE将展示如何用最少的投入通过补充文档来增进沟通、提高人们对核心的认识并使之把开发工作集中到CORE上来……

15.4 模式:DOMAIN VISION STATEMENT

在项目开始时,模型通常并不存在,但是模型开发的需求是早就确定下来的重点。在后面的开发阶段,我们需要解释清楚系统的价值,但这并不需要深入地分析模型。此外,领域模型的关键方面可能跨越多个BOUNDED CONTEXT,而且从定义上看,无法将这些彼此不同的模型组织起来表明其共同的关注点。

很多项目团队都会编写“愿景说明”以便管理。最好的愿景说明会展示出应用程序为组织带来的具体价值。一些愿景说明会把创建领域模型当作一项战略资产。通常,愿景说明文档在项目启动以后就被弃之不用了,而在实际开发过程中从来不会使用它,甚至根本不会有技术人员去阅读它。

DOMAIN VISION STATEMENT就是模仿这类文档创建的,但它关注的重点是领域模型的本质,以及如何为企业带来价值。在项目开发的所有阶段,管理层和技术人员都可以直接用领域愿景说明来指导资源分配、建模选择和团队成员的培训。如果领域模型为多个群体提供服务,那么此文档还能够显示出他们的利益是如何均衡的。

因此:

写一份CORE DOMAIN的简短描述(大约一页纸)以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方利益的。这份描述要尽量精简。尽早把它写出来,随着新的理解随时修改它。

DOMAIN VISION STATEMENT可以用作一个指南,它帮助开发团队在精炼模型和代码的过程中保持统一的方向。团队中的非技术成员、管理层甚至是客户也都可以共享领域愿景说明(当然,包含专有信息的情况除外)。

以下两个表格分别包含了航班预定系统和半导体工厂自动化系统的DOMAIN VISION STATEMENT。

以下内容是DOMAIN VISION STATEMENT的一部分 以下内容虽然很重要,但它不是DOMAIN VISION STATEMENT的一部分 第15章 精炼 - 图5

以下内容是DOMAIN VISION STATEMENT的一部分 以下内容虽然很重要,但它不是DOMAIN VISION STATEMENT的一部分 第15章 精炼 - 图6

DOMAIN VISION STATEMENT为团队提供了统一的方向。但在高层次的说明和代码或模型的完整细节之间通常还需要做一些衔接……

15.5 模式:HIGHLIGHTED CORE

DOMAIN VISION STATEMENT从宽泛的角度对CORE DOMAIN进行了说明,但它把什么是具体核心模型元素留给人们自己去解释和猜测。除非团队的沟通极其充分,否则单靠VISION STATEMENT是很难产生什么效果的。

尽管团队成员可能大体上知道核心领域是由什么构成的,但CORE DOMAIN中到底包含哪些元素,不同的人会有不同的理解,甚至同一个人在不同的时间也会有不同的理解。如果我们总是要不断过滤模型以便识别出关键部分,那么就会分散本应该投入到设计上的精力,而且这还需要广泛的模型知识。因此,CORE DOMAIN必须要很容易被分辨出来。

对代码所做的重大结构性改动是识别CORE DOMAIN的理想方式,但这些改动往往无法在短期内完成。事实上,如果团队的认识还不够全面,这样的重大代码修改是很难进行的。

通过修改模型的组织结构(如划分GENERIC SUBDOMAIN和本章后面要介绍的一些改动),可以用MODULE表达出核心领域。但如果把它作为表达CORE DOMAIN的唯一方法,那么对模型的改动会很大,因此很难马上看到结果。

我们可能需要用一种轻量级的解决方案来补充这些激进的技术手段。可能有一些约束使你无法从物理上分离出CORE,或者你可能是从已有代码开始工作的,而这些代码并没有很好地区分出CORE,但你确实很需要知道什么是CORE并建立起共识,以便有效地通过重构进行更好的精炼。即使到了高级阶段,通过仔细挑选几个图或文档,也能够为团队提供思考的定位点和切入点。

无论是使用了详尽的UML模型的项目,还是那些只使用很少的外部文档并且把代码用作主要的模型存储库的项目(如XP项目),都会面临这些问题。极限编程团队可能采用更简洁的做法,他们更少地使用这些补充解决方案,而且只是临时使用(例如,在墙上挂一张手绘的图,让所有人都能看到),但这些技术可以很好地结合到开发过程中。

把模型的一个特别部分连同它的实现一起区分出来,这只是对模型的一种反映,而不必是模型自身的一部分。任何使人们易于了解CORE DOMAIN的技术都可以采用。这类解决方案有两种典型的代表性技术。

15.5.1 精炼文档

我经常会创建一个单独的文档来描述和解释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更明显,更易于管理。

15.6 模式:COHESIVE MECHANISM

封装机制是面向对象设计的一个基本原则。把复杂算法隐藏到方法中,再为方法起一个一看就知道其用途的名字,这样就把“做什么”和“如何做”分开了。这种技术使设计更易于理解和使用。然而它也有一些先天的局限性。

计算有时会非常复杂,使设计开始变得膨胀。机械性的“如何做”大量增加,把概念性的“做什么”完全掩盖了。为解决问题提供算法的大量方法掩盖了那些用于表达问题的方法。

这种方法的扩散是模型出问题的一种症状。这时应该通过重构得到更深层的理解,从而找到更适合解决问题的模型和设计元素。首先要寻找的解决方案是找到一个能使计算机制变得简单的模型。但有时我们会发现,有些计算机制本身在概念上就是内聚的。这种内聚的计算概念可能并不包括我们所需的全部计算。我们讨论的也不是一种万能的计算器。把内聚部分提取出来会使剩下的部分更易于理解。

因此:

把概念上的COHESIVE MECHANISM(内聚机制)分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。用一个INTENTION-REVEALING INTERFACE来暴露这个框架的功能。现在,领域中的其他元素就可以只专注于如何表达问题(做什么)了,而把解决方案的复杂细节(如何做)转移给了框架。

然后,这些被分离出来的机制承担起支持的任务,从而留下一个更小的、表达得更清楚的CORE DOMAIN,这个核心以更加声明式的方式通过接口来使用这些机制。

把标准的算法或公式识别出来以后,可以把一部分设计的复杂性转移到一系列已经过深入研究的概念中。在这种方法的引导下,我们可以放心地实现一个解决方案,而且只需进行很少的尝试和改错。我们可以依靠其他一些了解这种算法或至少能够查到相关资料的开发人员。这个好处类似于从公开发布的GENERIC SUBDOMAIN模型获得的好处,但找到完备的算法或公式计算的机会比利用通用子领域模型的机会更大一些,因为这种水平的计算机科学已经有了较深入的研究。但是,我们仍常常需要创建新的算法。创建的算法应该主要用于计算,避免在算法中混杂用于表达问题的领域模型。二者的职责应该分离。CORE DOMAIN或GENERIC SUBDOMAIN的模型描述的是事实、规则或问题。而COHESIVE MECHANISM则用来满足规则或者用来完成模型指定的计算。

示例 从组织结构图中分离出一个COHESIVE MECHANISM

我曾经在一个项目上经历过这种分离过程,这个项目需要一种非常详细的组织结构图模型。这个模型可以表示出一个人正在为谁工作以及他属于哪个分支部门,模型还提供了一个接口,通过这个接口可以提出和回答相关的问题。由于大部分问题都类似于“在这个指挥链中谁有权批准这件事”或“在这个部门中谁能够处理这样的问题”,因此团队意识到大部分复杂性都来自于遍历组织树中的特定分支,从中搜索特定的人员或关系。这恰好是成熟的图系统所能够解决的问题,图是一个由弧连接的节点集合(弧叫做边)以及遍历图所需的规则和算法组成。

负责这项工作的开发人员开发出了一个图的遍历框架,并把它实现为一种COHESIVE MECHANISM。这个框架使用了标准的图术语和算法,大多数计算机专业人员都很熟悉这些术语和算法,而且它们在教科书中也大量出现。这位开发人员并没有实现一个完整的概念框架,而只是实现了它的一个子集,该子集涵盖了组织模型所需的功能。而且由于采用了INTENTION-REVEALING INTERFACE,因此获取答案的方式并不是我们主要关心的问题。

现在,组织模型可以用标准的图术语简单地把每个人表示为一个节点,把人们之间的关系表示为连接这些节点的边(弧)。这样,使用这个图框架机制就可以找到任意两个人之间的关系了。

如果这个机制被混杂到领域模型中,那么将会产生两个后果。一是模型会与一个用于解决问题的特殊方法耦合在一起,这将限制将来的选择。更重要的是,组织的模型将变得异常复杂和混乱。把该机制与模型分开的好处是可以用声明式的风格来描述组织,使组织结构变得更清晰。而且用于图操作的复杂代码被分离到一个单纯的、基于成熟算法的机制框架中,从而可以进行单独的维护和单元测试。

COHESIVE MECHANISM的另一个例子是用一个框架来构造SPECIFICATION对象,并为这些对象所需的基本的比较和组合操作提供支持。利用这个框架,CORE DOMAIN和GENERIC SUBDOMAIN可以用SPECIFICATION模式中所描述的清晰的、易于理解的语言来声明它们的规格(参见第10章)。这样,比较和组合等复杂操作可以留给框架去完成。

15.6.1 GENERIC SUBDOMAIN与COHESIVE MECHANISM的比较

GENERIC SUBDOMAIN与COHESIVE MECHANISM的动机是相同的——都是为CORE DOMAIN减负。区别在于二者所承担的职责的性质不同。GENERIC SUBDOMAIN是以描述性的模型作为基础的,它用这个模型表示出团队会如何看待领域的某个方面。在这一点上它与CORE DOMAIN没什么区别,只是重要性和专门程度较低而已。COHESIVE MECHANISM并不表示领域,它的目的是解决描述性模型所提出来的一些复杂的计算问题。

模型提出问题,COHESIVE MECHANISM解决问题。

在实践中,除非你识别出一种正式的、公开发布的算法,否则这种区别通常并不十分清楚,至少在开始时是这样。在后续的重构中,如果发现一些先前未识别的模型概念会使这种机制变得更为简单,那么就可以把这种算法精炼成一种更纯粹的机制,或者转换为一个GENERIC SUBDOMAIN。

15.6.2 MECHANISM是CORE DOMAIN一部分

我们几乎总是想要把MECHANISM从CORE DOMAIN中分离出去。例外的情况是MECHANISM本身就是专有的并且是软件的一项核心价值。有时,非常专用的算法就是这种情况。例如,如果一个非常高效的算法(用于计算日程安排)是运输物流应用程序中的标志性特性之一,那么该机制就可以被认为是概念核心的一部分。我以前参加过一个投资银行的项目,在这个项目中有一个非常专业的风险评估算法,它无疑是CORE DOMAIN的一部分(事实上,这个算法是高度机密的,甚至大部分核心开发人员都看不到它们)。当然,这些算法可能是一个用于预测风险的规则集的特殊实现。通过更深入的分析可能会得到一个更深层的模型,从而用一种封装的解决机制把这些规则显式地表达出来。

但那只是将来要做的进一步改进。是否做这个决定取决于成本—效益分析。实现新设计的难度有多大?当前设计有多难理解和修改?采用更高级的设计后,对从事这些工作的人来说,设计会得到多大程度的简化?当然,有人对新模型的组成有什么想法吗?

示例 绕了一圈,MECHANISM又重新回到组织结构图中

实际上,在我们完成了前面示例中的组织模型一年之后,其他开发人员又重新设计了它,取消了分离的图框架。他们认为对象数量在增加,而且把这种MECHANISM分离到单独的包中也会变得很复杂,于是觉得这二者没必要如此。相反,他们把节点行为添加到组织ENTITY的父类中。但他们保留了组织模型的声明式公共接口。他们甚至在组织ENTITY中保持了MECHANISM的封装。

绕弯路之后又返回到原来的老路上是很常见的事情,但并不会退回到起点。最终结果通常是得到了一个更深层的模型,这个模型能够更清楚地区分出事实、目标和MECHANISM。实用的重构在保留中间阶段的重要价值的同时还能够去除不必要的复杂性。

15.7 通过精炼得到声明式风格

声明式设计和“声明式风格”是第10章的一个主题,但在本章的战略精炼这个话题上,有必要特别提一下这种设计风格。精炼的价值在于使你能够看到自己正在做什么,不让无关细节分散你的注意力,并通过不断削减得到核心。如果领域中那些起到支持作用的部分提供了一种简练的语言,可用于表示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划分出来。

15.8 模式: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的工作。

15.8.1 创建SEGREGATED CORE的代价

有时候,把CORE分离出来会使得它与那些紧密耦合的非CORE类的关系变得更晦涩,甚至更复杂,但CORE DOMAIN更清晰了,而且更易于处理,因此获得的好处还是足以抵偿这种代价。

SEGREGATED CORE使我们能够提高CORE DOMAIN的内聚性。我们可以使用很多有意义的方式来分解模型,有时在创建SEGREGATED CORE时,可以把一个内聚性很好的MODULE拆分开,通过牺牲这种内聚性来换取CORE DOMAIN的内聚性。这样做是值得的,因为企业软件的最大价值来自于模型中企业的那些特有方面。

当然,另一个代价是分离CORE需要付出很大的工作量。我们必须认识到,在做出SEGREGATED CORE的决定时,有可能需要开发人员对整个系统做出修改。

当系统有一个很大的、非常重要的BOUNDED CONTEXT时,但模型的关键部分被大量支持性功能掩盖了,那么就需要创建SEGREGATED CORE了。

15.8.2 不断发展演变的团队决策

就像很多战略设计决策所要求的一样,创建SEGREGATED CORE需要整个团队一致行动。这一行动需要团队的一致决策,而且团队必须足够自律和协调才能执行这样的决策。困难之处在于既要约束每个人使其都使用相同的CORE定义,又不能一成不变地去执行这个决策。由于CORE DOMAIN也是不断演变的(像任何其他设计方面一样),在处理SEGREGATED CORE的过程中我们会不断积累经验,这将使我们对什么是核心什么是支持元素这些问题产生新的理解。我们应该把这些理解反馈到设计中,从而得到更完善的CORE DOMAIN和SEGREGATED CORE MODULE的定义。

这意味着新的理解必须持续不断地在整个团队中共享,但个人(或编程对)不能单方面根据这些理解擅自采取行动。无论团队采用了什么样的决策过程,团队一致通过也好,由领导者下命令决定也好,决策过程都必须具有足够的敏捷性,可以反复纠正。团队必须进行有效的沟通,以便使每个人都共享同一个CORE视图。

示例 把货物运输模型的CORE分离出来

我们从图15-2所示的模型开始,把它作为货物运输调度软件的基础。

第15章 精炼 - 图7 图15-2

注意,与实际应用程序所需的模型相比,这个模型是高度简化的。真实的模型过于复杂,不适合作为例子。因此,尽管这个示例的复杂程度可能不足以驱使我们创建SEGREGATED CORE,但可以把这个模型想象得十分复杂,很难解释,而且无法作为一个整体来处理。

现在,运输模型的实质是什么?通常“梗概”是一个很好的起点。据此,我们可能会注意到Pricing(定价)和Invoice(发票)上[4]。但实际上我们需要看一下DOMAIN VISION STATEMENT。以下就是从愿景说明中摘录的:

……提高操作的可见性,并提供更快速可靠地满足客户需求的工具……

这个应用程序并不是为销售部门设计的,而是供公司一线操作人员使用。因此,我们把所有与金钱有关的问题(当然很重要)归结为支持性作用。已经有人把一些这样的项放到一个单独的包(Billing)中。我们可以保留这个包,并进一步确认它起到支持作用。

我们需要把重点放在货物处理上:根据客户需求来运输货物。我们把与这些活动直接相关的类提取出来放到一个新的包Delivery中,这样就产生了一个SEGREGATED CORE,如图15-3所示。

大部分操作都只是把类移动到新的包中,但模型本身也有几处改动。

首先,Customer Agreement对Handling Step进行了约束。这是团队在分离CORE过程中获得的典型理解。由于团队把注意力放在有效、正确的运输上,显然Customer Agreement中的运输约束是非常重要的,而且应该在模型中显式地表达出来。

另一项更改更有实效。在重构之后的模型中,Customer Agreement直接连接到Cargo,而不再需要通过Customer进行导航(在预订Cargo时,Customer Agreement必须像Customer一样连接到Cargo)。在实际运输时,Customer与运输作业的关系不如Agreement与作业的关系紧密。而在原来的模型中,必须根据Customer在运输中的角色找到正确的Customer,然后再查询其Customer Agreement。这种交互使得模型的表述不易理解。新的关联使那些最重要的场景变得尽可能简单和直接。现在就很容易把Customer完全从CORE中分离出去了。

那么到底是否应该把Customer提取出来呢?我们的关注点是要满足Customer的需求,因此最初看上去Customer应该属于CORE。然而,由于运输期间的交互现在可以直接访问Customer Agreement了,因此就不再需要Customer类。这样Customer的基本模型就非常通用了。

Leg是否应该保留在CORE中这个问题可能会引起很大的争议。我的意见是CORE应保持最小化,而且Leg与Transport Schedule、Routing Service和Location具有更紧密的联系,而这三者都不需要在CORE中。但是,如果这个模型描述的很多场景都涉及Leg,那么我就会把它移到Delivery包中,即使把它与上面那些类分开显得有些不协调。

在这个例子中,所有类定义都与先前相同,但精炼通常都需要对类进行重构,以便分离出通用职责和领域专有职责,然后就可以把核心分离出来了。

既然我们已经有了一个SEGREGATED CORE,重构就完成了。但剩下的Shipping包正是“把CORE提取出来后剩下的所有东西”。我们可以再进行其他的重构过程,以便得到更清晰的打包方式,如图15-4所示。

第15章 精炼 - 图8 图15-3 按照客户需求可靠地运输货物是这个项目的核心目标

第15章 精炼 - 图9 图15-4 完成SEGREGATED CORE之后留下的有意义的非CORE子领域MODULE

这种效果不是一次就能实现的,可能需要经过多次重构。于是,我们最后得到了一个SEGREGATED CORE包、一个GENERIC SUBDOMAIN和两个起支持作用的领域专用包。在有了更深层的理解后,可能会为Customer创建一个GENERIC SUBDOMAIN,或者将Customer专用于运输。

识别有用的、有意义的MODULE是一项建模活动(正如第5章中所讨论的那样)。开发人员和领域专家在战略精炼中进行协作,这种协作是知识消化过程的一部分。

15.9 模式:ABSTRACT CORE

第15章 精炼 - 图10

通常,即便是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是用代码编写的,因此更为严格和完整。

15.10 深层模型精炼

精炼并不仅限于从整体上把领域中的一些部分从CORE中分离出来。它也意味着对子领域(特别是CORE DOMAIN)进行精炼,通过持续重构得到更深层的理解,从而向深层模型和柔性设计推进。精炼的目标是把模型设计得更明显,使我们可以用模型简单地把领域表示出来。深层模型把领域中最本质的方面精炼成一些简单的元素,使我们可以把这些元素组合起来解决应用程序中的重要问题。

尽管任何带来深层模型的突破都有价值,但只有CORE DOMAIN中的突破才能改变整个项目的轨道。

15.11 选择重构目标

当你遇到一个杂乱无章的大型系统时,应该从哪里入手呢?在XP社区中,答案往往是以下之一:

(1) 可以从任何地方开始,因为所有的东西都要进行重构;

(2) 从影响你工作的那部分开始——也就是完成具体任务所需要的那个部分。

这两种做法我都不赞成。第一种做法并不十分可行,只有少数完全由顶尖的程序员组成的团队才是例外。第二种做法往往只是对外围问题进行了处理,只治其标而不治其本,回避了最严重的问题。最终这会使代码变得越来越难以重构。

因此,如果你既不能全面解决问题,又不能“哪儿痛治哪儿”,那么该怎么办呢?

(1) 如果采用“哪儿痛治哪儿”这种重构策略,要观察一下根源问题是否涉及CORE DOMAIN或CORE与支持元素的关系。如果确实涉及,那么就要接受挑战,首先修复核心。

(2) 当可以自由选择重构的部分时,应首先集中精力把CORE DOMAIN更好地提取出来,完善对CORE的分离,并且把支持性的子领域提炼成通用子领域。

以上就是如何从重构中获取最大利益的方法。