第16章 大型结构

硅谷一家小设计公司签了一份为卫星通信系统创建模拟器的合同。工作进展得很顺利,他们正在开发一个MODEL-DRIVEN DESIGN,这个设计能够表示和模拟各种网络条件和故障。

第16章 大型结构 - 图1

数千人分工合作来制作“艾滋病纪念拼被”(AIDS Quilt)

但开发团队的领导者却有点不安。问题本身太复杂了。为了澄清模型中的复杂关系,他们已经把设计分解为一些在规模上便于管理的内聚MODULE,于是现在便有了的很多MODULE。在这种情况下,开发人员要想查找某个功能,应该到哪个MODULE中去查呢?如果有了一个新类,应该把它放在哪里?这些小软件包的实际意义是什么?它们又是如何协同工作的呢?而且以后还要创建更多的MODULE。

开发人员互相之间仍然能够进行很好的沟通,而且也知道每天都要做什么工作,但项目领导者却不满足这种一知半解的状态。他们需要某种组织设计的方式,以便在项目进入到更复杂的阶段时能够理解和掌控它。

他们进行了头脑风暴活动,发现了很多潜在的办法。开发人员提出了不同的打包方案。有一些文档给出了系统的全貌,还有一些使用建模工具绘制的类图——新视图可以用来指引开发人员找到正确的模块。但项目领导者对这些小花招并不满意。

他们可以用模型把模拟器的工作流程简单地描述出来,也可以说清楚基础设施是如何序列化数据的,以及电信技术层怎样保证数据的完整性和路由选择。模型中包含了所有细节,却没有一条清楚的主线。

领域的一些重要概念丢失了。但这次丢失的不是对象模型中的一两个类,而是整个模型的结构。

经过一两周的仔细思考之后,开发人员有了思路。他们打算把设计放到一个结构中。整个模拟器将被看作由一系列层组成,这些层分别对应于通信系统的各个方面。最下面的层用来表示物理基础设施,它具有将数据位从一个节点传送到另一个节点的基本能力。它的上面是封包路由层,与数据流定向有关的问题都被集中到这一层中。其他的层则表示其他概念层次的问题。这些层共同描述了系统的大致情况。

他们开始按照新的结构来重构代码。为了不让MODULE跨越多个层,必须对它们重新定义。在一些情况下,还需要重构对象职责,以便明确地让每个对象只属于一个层。另一方面,藉由应用这些新思路的实际经验,概念层本身的定义也得到了精化。层、MODULE和对象一起演变,最后,整个设计都符合了这种分层结构的大体轮廓。

这些层并不是MODULE,也不是任何其他的代码工件。它们是一种全局性的规则集,用于约束整个设计中的任何MODULE或对象(甚至包括与其他系统的接口)的边界和关系。

实施了这种分层级别之后,设计重新变得易于理解了。人们基本上知道到哪里去寻找某个特定功能。分工不同的开发人员所做的设计决策可以大体上互相保持一致。这样就可以处理更加复杂的设计了。

即使将MODULE分解,一个大模型的复杂性也可能会使它变得很难掌握。MODULE确实把设计分解为更易管理的小部分,但MODULE的数量可能会很多。此外,模块化并不一定能够保证设计的一致性。对象与对象之间,包与包之间,可能应用了一堆的设计决策,每个决策看起来都合情合理,但总的来看却非常怪异。

严格划分BOUNDED CONTEXT可能会防止出现破坏和混淆,但其本身对于从整体上审视系统并无任何助益。

精炼可以帮助我们把注意力集中于CORE DOMAIN,并将子领域分离出来,让它们承担支持性的职责。但我们仍然需要理解这些支持性元素,以及它们与CORE DOMAIN的关系,还有它们互相之间的关系。理想的情况是,整个CORE DOMAIN非常清楚和易于理解,因此不再需要额外的指导,但我们并不总能处于这样好的境况中。

无论项目的规模如何,人们总需要有各自的分工,来负责系统的不同部分。如果没有任何协调机制或规则,那么相同问题的各种不同风格和截然不同的解决方案就会混杂在一起,使人们很难理解各个部分是如何组织在一起的,也不可能看到整个系统的统一视图。从设计的一个部分学到的东西并不适用于这个设计的其他部分,因此项目最后的结果是开发人员成为各自MODULE的专家,一旦脱离了他们自己的小圈子就无法互相帮助。在这种情况下,CONTINUOUS INTEGRATION根本无法实现,而BOUNDED CONTEXT也使项目变得支离破碎。

在一个大的系统中,如果因为缺少一种全局性的原则而使人们无法根据元素在模式(这些模式被应用于整个设计)中的角色来解释这些元素,那么开发人员就会陷入“只见树木,不见森林”的境地。

我们需要理解各个部分在整体中的角色,而不必去深究细节。

“大型结构”是一种语言,人们可以用它来从大局上讨论和理解系统。它用一组高级概念或规则(或两者兼有)来为整个系统的设计建立一种模式。这种组织原则既能指导设计,又能帮助理解设计。另外,它还能够协调不同人员的工作,因为它提供了共享的整体视图,让人们知道各个部分在整体中的角色。

设计一种应用于整个系统的规则(或角色和关系)模式,使人们可以通过它在一定程度上了解各个部分在整体中所处的位臵(即使是在不知道各个部分的详细职责的情况下)。

这种结构可以被限制在一个BOUNDED CONTEXT中,但通常情况下它会跨越多个BOUNDED CONTEXT,并通过提供一种概念组织把项目涉及的所有团队和子系统紧密结合到一起。好的结构可以帮助人们深入地理解模型,还能够对精炼起到补充作用。

第16章 大型结构 - 图2 图16-1 一些大型结构模式

大部分大型结构都无法用UML来表示,而且也不需要这样做。这些大型结构是用来勾画和解释模型及设计的,但在设计中并不出现,它们只是用来表达设计的另外一种方式。在本章的示例中,你将看到许多添加了大型结构信息的非正式的UML图。

当团队规模较小而且模型也不太复杂时,只需将模型分解为合理命名的MODULE,再进行一定程度的精炼,然后在开发人员之间进行非正式的协调,以上这些就足以使模型保持良好的组织结构了。

大型结构可以节省项目的开发费用,但不适当的结构会严重妨碍开发的进展。本章将探讨一些能成功构建这种设计结构的模式。

16.1 模式:EVOLVING ORDER

很多开发人员都亲身经历过由于设计结构混乱而产生的代价。为了避免混乱,项目通过架构从各个方面对开发进行约束。一些技术架构确实能够解决技术问题,如网络或数据持久化问题,但当我们在应用层和领域模型中使用架构时,它们可能会产生自己的问题。它们往往会妨碍开发人员创建适合于解决特定问题的设计和模型。一些要求过高的架构甚至会妨碍编程语言本身的使用,导致应用程序开发人员根本无法使用他们在编程语言中最熟悉的和技术能力很强的一些功能。而且,无论架构是面向技术的,还是面向领域的,如果其限定了很多前期设计决策,那么随着需求的变更和理解的深入,这些架构会变得束手束脚。

近年来,一些技术架构(如J2EE)已经成为主流技术,而人们对领域层中的大型结构却没有做多少研究,这是因为应用程序不同,其各自的需求也大为不同。

在项目前期使用大型结构可能需要很大的成本。随着开发的进行,我们肯定会发现更适当的结构,甚至会发现先前使用的结构妨碍了我们采取一种使应用程序更清晰和简化的路线。这种结构的一部分是有用的,但却使你失去了其他很多机会。你的工作会慢下来,因为你要寻找解决的办法或试着与架构师们进行协商。但经理会认为架构已经定下来了,当初选这个架构就是因为它能够使应用程序变得简单一些,那为什么不去开发应用程序,却在这些架构问题上纠缠不清呢?即使经理和架构团队能够接受这些问题,但如果每次修改都像是一场攻坚战,那么人们很快就会疲乏不堪。

一个没有任何规则的随意设计会产生一些无法理解整体含义且很难维护的系统。但架构中早期的设计假设又会使项目变得束手束脚,而且会极大地限制应用程序中某些特定部分的开发人员/设计人员的能力。很快,开发人员就会为适应结构而不得不在应用程序的开发上委曲求全,要么就是完全推翻架构而又回到没有协调的开发老路上来。

问题并不在于指导规则本身应不应该存在,而在于这些规则的严格性和来源。如果这些用于控制设计的规则确实符合开发环境,那么它们不但不会阻碍开发,而且还会推动开发在健康的方向上前进,并且保持开发的一致性。

因此:

让这种概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同的结构风格。不要依此过分限制详细的设计和模型决策,这些决策和模型决策必须在掌握了详细知识之后才能确定。

有时个别部分具有一些很自然且有用的组织和表示方式,但这些方式并不适用于整体,因此施加全局规则会使这些部分的设计不够理想。在选择大型结构时,应该侧重于整体模型的管理,而不是优化个别部分的结构。因此,在“结构统一”和“用最自然的方式表示个别组件”之间需要做出一些折中选择。根据实际经验和领域知识来选择结构,并避免采用限制过多的结构,如此可以降低折中的难度。真正适合领域和需求的结构能够使细节的建模和设计变得更容易,因为它快速排除了很多选项。

大型结构还能够为我们做设计决策提供捷径,虽然原则上也可以通过研究各个对象来做出这些决策,但实际上这会耗费太长时间,而且产生的结果可能不一致。当然,持续重构仍然是必要的,但这种结构可以帮助重构变得更易于管理,并使不同的人能够得到一致的解决方案。

大型结构通常需要跨越BOUNDED CONTEXT来使用。在经历了实际项目上的迭代之后,结构将失去与特定模型紧密联系的特性,也会得到符合领域的CONCEPTUAL CONTOUR的特性。这并不意味着它不能对模型做出任何假设,而是说它不会把专门针对局部情况而做的假设强加于整个项目。它应该为那些在不同CONTEXT中工作的开发团队保留一定的自由,允许他们为了满足局部需要而修改模型。

此外,大型结构必须适应开发工作中的实际约束。例如,设计人员可能无法控制系统的某些部分的模型,特别是外部子系统或遗留子系统。这个问题有多种解决方式,如修改结构使之更适应特定外部元素,或者指定应用程序与外部元素的关联方式,或者使结构变得足够松散,以灵活应对难以处理的现实情况。

与CONTEXT MAP不同的是,大型结构是可选的。当使用某种结构可以节省成本并带来益处时,并且发现了一种适当的结构,就应该使用它。实际上,如果一个系统简单到把它分解为MODULE就足以理解它,那么就不必使用这种结构了。当发现一种大型结构可以明显使系统变得更清晰,而又没有对模型开发施加一些不自然的约束时,就应该采用这种结构。使用不合适的结构还不如不使用它,因此最好不要为了追求设计的完整性而勉强去使用一种结构,而应该找到尽可能精简的方式解决所出现问题。要记住宁缺勿滥的原则。

大型结构可能非常有帮助,但也有少数不适用的情况,这些例外情况应该以某种方式被标记出来,以便让开发人员知道在没有特殊注明时可以遵循这种结构。如果不适用的情况开始大量出现,就要修改这种结构了,或者干脆不用它。

如前所述,要想创建一种既为开发人员保留必要自由度同时又能保证开发工作不会陷入混乱的结构绝非易事。尽管人们已经在软件系统的技术架构上投入了大量工作,但有关领域层的结构化研究还很少见。一些方法会破坏面向对象的范式,如那些按应用任务或按用例对领域进行分解的方法。整个领域的研究还很贫瘠。我曾经在一些项目上看到过几个通用的大型结构模式。本章将讨论4种模式,其中可能会有一种符合你的需要,或者能够为你提供一些思路,从而找到一种适合你的项目的结构。

16.2 模式:SYSTEM METAPHOR

隐喻思维在软件开发(特别是模型)中是很普遍的。但极限编程中的“隐喻”却具有另外一种含义,它用一种特殊的隐喻方式来使整个系统的开发井然有序。

一栋大楼的防火墙能够在周围发生火灾时防止火势从其他建筑蔓延到它自身,同样,软件“防火墙”可以保护局部网络免受来自更大的外部网的破坏。这个“防火墙”的隐喻对网络架构产生了很大影响,并且由此而产生了一整套产品类别。有多种互相竞争的防火墙可供消费者选择,它们都是独立开发的,而且人们知道它们在一定程度上可以互换。即使网络的初学者也很容易掌握这个概念。这种在整个行业和客户中的共同理解很大一部分上得益于隐喻。

然而这个类比却并不准确,而且防火墙从功能上来看也是把双刃剑。防火墙的隐喻引导人们开发出了软件屏障,但有时它并不能起到充分的防护作用,而且会阻止正当的数据交换,同时也无法防护来自网络内部的威胁。例如,无线LAN就存在漏洞。防火墙这个形象的隐喻确实很有用,但所有隐喻也都是有弊端的[5]

软件设计往往非常抽象且难于掌握。开发人员和用户都需要一些切实可行的方式来理解系统,并共享系统的一个整体视图。

从某种程度上讲,隐喻对人们的思考方式有着深刻地影响,它已经渗透到每个设计中。系统有很多“层”,层与层之间依次叠放起来。系统还有“内核”,位于这些层的“中心”。但有时隐喻可以传达整个设计的中心主题,并能够在团队所有成员中形成共同理解。

在这种情况下,系统实际上就是由这个隐喻塑造的。开发人员所做的设计决策也将与系统隐喻保持一致。这种一致性使其他开发人员能够根据同一个隐喻来解释复杂系统中的多个部分。开发人员和专家在讨论时有一个比模型本身更具体的参考点。

SYSTEM METAPHOR(系统隐喻)是一种松散的、易于理解的大型结构,它与对象范式是协调的。由于系统隐喻只是对领域的一种类比,因此不同模型可以用近似的方式来与它关联,这使得人们能够在多个BOUNDED CONTEXT中使用系统隐喻,从而有助于协调各个BOUNDED CONTEXT之间的工作。

SYSTEM METAPHOR是极限编程的核心实践之一,因此它已经成为一种非常流行的方法(Beck 2000)。遗憾的是,很少有项目能够找到真正有用的METAPHOR,而且人们有时还会把一些起反作用的隐喻思想灌输到领域中。有时使用太强的隐喻反而会有风险,因为它使设计中掺杂了一些与当前问题无关的类比,或者是类比虽然很有吸引力,但它本身并不恰当。

尽管如此,SYSTEM METAPHOR仍然是众所周知的大型结构,它对一些项目非常有用,而且很好地说明了结构的总体概念。

因此:

当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向着一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。围绕这个隐喻来组织设计,并把它吸收到UBIQUITOUS LANGUAGE中。SYSTEM METAPHOR应该既能促进系统的交流,又能指导系统的开发。它可以增加系统不同部分之间的一致性,甚至可以跨越不同的BOUNDED CONTEXT。但所有隐喻都不是完全精确的,因此应不断检查隐喻是否过度或不恰当,当发现它起到妨碍作用时,要随时准备放弃它。

“幼稚隐喻”以及我们为什么不需要它

由于在大多数项目并不会自动出现有用的隐喻,因此XP社区中的一些人开始谈论“幼稚隐喻”(Naive Metaphor),他们所说的幼稚隐喻就是领域模型本身。

这个术语的一个问题在于,一个成熟的领域模型绝对不会是“幼稚的”。实际上,“工资处理就像一条装配线”这个隐喻与模型的实际情况相比要幼稚得多,因为模型是软件开发人员与领域专家进行了多次知识消化的迭代过程才得到的,它已经紧密结合到应用程序的实现中,并经过了实践的检验。

“幼稚隐喻”这个术语应该停止使用了。

SYSTEM METAPHOR并不适用于所有项目。从总体上讲,大型结构并不是必须要用的。在极限编程的12个实践中,SYSTEM METAPHOR的角色可以由UBIQUITOUS LANGUAGE来承担。当项目中发现一种非常合适的SYSTEM METAPHOR或其他大型结构时,应该用它来补充UBIQUITOUS LANGUAGE。

16.3 模式:RESPONSIBILITY LAYER

在本书从头至尾的讨论中,单独的对象被分配了一组相关的、范围较窄的职责。职责驱动的设计在更大的规模上也适用。

如果每个对象的职责都是人为分配的,将没有统一的指导原则和一致性,也无法把领域作为一个整体来处理。为了保持大模型的一致,有必要在职责分配上实施一定的结构化控制。

当对领域有了深入的理解后,大的模式会变得清晰起来。一些领域具有自然的层次结构。某些概念和活动处在其他元素形成的一个大背景下,而那些元素会因不同原因且以不同频率独立发生变化。如何才能充分利用这种自然结构,使它变得更清晰和有用呢?这种自然的层次结构使我们很容易想到把领域分层,这是最成功的架构设计模式之一([Buschmann et al.1996]等)。

所谓的层,就是对系统进行划分,每个层的元素都知道或能够使用在它“下面”的那些层的服务,但却不知道它“上面”的层,而且与它上面的层保持独立。当我们把MODULE的依赖性画出来时,图的布局通常是具有依赖性的MODULE出现在它所依赖的模块上面。按照这种方式,可以将各层的顺序梳理出来,最终,低层中的对象在概念上不依赖于高层中的对象。

这种自发的分层方式虽然使跟踪依赖性变得更容易,而且有时具有一定的直观意义,但它对模型的理解并没有多大的帮助,也不能指导建模决策。我们需要一种具有更明确目的的分层方式。

第16章 大型结构 - 图3 图16-2 自发的分层,这些包描述了什么事情

在一个具有自然层次结构的模型中,可以围绕主要职责进行概念上的分层,这样可以把分层和职责驱动的设计这两个强有力的原则结合起来使用。

这些职责必须比分配给单个对象的职责广泛得多才行,我们稍后就会举例说明这一点。当设计单独的MODULE和AGGREGATE时,要将其限定在其中一个主要职责上。这种明确的职责分组可以提高模块化系统的可理解性,因为MODULE的职责会变得更易于解释。而高层次的职责与分层的结合为我们提供了一种系统的组织原则。

分层模式有一种变体最适合按职责来分层,我们把这种变体称为RELAXED LAYERED SYSTEM(松散分层系统)[Buschmann et al.1996,p.45],如果采用这种分层模式,某一层中的组件可以访问任何比它低的层,而不限于只能访问直接与它相邻的下一层。

因此:

注意观察模型中的概念依赖性,以及领域中不同部分的变化频率和变化的原因。如果在领域中发现了自然的层次结构,就把它们转换为宽泛的抽象职责。这些职责应该描述系统的高层目的和设计。对模型进行重构,使得每个领域对象、AGGREGATE和MODULE的职责都清晰地位于一个职责层当中。

这是一段很抽象的描述,但通过几个示例就可以把它说清楚了。本章开头的卫星通信模拟器就对职责进行了分层。我曾经在各种领域(如生产控制和财务管理)中看到过使用RESPONSIBILITY LAYER(职责层)所产生的良好效果。

下面的示例详细研究了RESPONSIBILITY LAYER,我们可以通过这个例子来体会一下如何去发现任何一种大型结构,以及它是如何指导和约束建模与设计的。、

示例 深入研究运输系统的分层

让我们看一下把RESPONSIBILITY LAYER应用于前面几章所讨论的货运应用程序会有什么效果。

当我们现在又回到这个应用程序时,开发团队已经有了很大的进展,他们已经创建了一个MODEL-DRIVEN DESIGN,并且提炼出了一个CORE DOMAIN。但随着设计变得充实,他们在如何把所有部分协调为一个整体上遇到了麻烦。他们正在寻找一种能够显示出整个系统主题并且让每个人都达成一致看法的大型结构。

我们来看一下这个模型中有代表性的一个部分,如图16-3和图16-4所示。

第16章 大型结构 - 图4 图16-3 货运路线的一个基本的运输领域模型

第16章 大型结构 - 图5 图16-4 在预订期间使用模型来制定一个货运路线

团队成员研究运输领域已经有好几个月了,并且已经观察到了一些自然的概念层次结构。他们发现在讨论运输时间表(安排好的货轮航次或火车班次)时不需要涉及所运输的货物。而当讨论对一个货物的跟踪时,如果不知道它的运输信息,那么就很难进行跟踪。概念依赖性是非常清楚的。团队很容易就区分出两个层:“作业”层和这些作业的基础层(他们把这个层叫做“能力”层)。

“作业”职责

公司的活动,无论是过去、现在还是计划的活动,都被组织到“作业”层中。最明显的作业对象是Cargo,它是公司大部分日常活动的焦点。Route Specification是Cargo的一个不可缺少的部分,它规定了运输需求。Itinerary是运输计划。这些对象都是Cargo聚合的一部分,它们的生命周期与一次进行中的运输活动紧密地联系在一起。

“能力”职责

这个层反映了公司在执行作业时所能利用的资源。Transit Leg就是一个典型的例子。人们为货轮制定航程时间表,货轮具有一定的货运能力,这个能力有可能被完全利用,也有可能未被完全利用。

当然,如果公司的主要业务是经营一个运输船队的话,那么Transit Leg将是作业层中的一员。但这个系统的用户并不需要关心这个问题(如果公司同时从事经营船队和经营货运这两种业务,并且希望协调它们,那么开发团队可能需要考虑不同的分层方案,或许要把作业层分成两个不同的层,如“运输作业”和“货物作业”。)

一个稍微复杂一点儿的决策是把Customer放在哪里。在一些企业中,客户只是一些临时对象。例如,在邮递公司中,只有在投递包裹的时候,才需要知道客户对象,投递完成之后,大部分客户就被忘记了,直到出现下一次投递。这种性质决定了在针对个人客户的包裹投递服务中,客户仅仅与作业相关。但在我们假想的这家运输公司中,需要与客户保持长期关系,而且大部分业务都来自回头客。考虑到企业用户的这些意图,Customer应该属于“能力”层。正如我们看到的,这并非一个技术决策,而是试图掌握并交流领域知识。

由于Cargo与Customer之间的关联可以限定在一个遍历方向,因此Cargo REPOSITORY需要通过一个查询来查找某个特定Customer的所有Cargo。不管怎样,按照这种方式来设计都有很好的理由,但在使用了大型结构以后,现在它变成一项必须要满足的需求了。

第16章 大型结构 - 图6 图16-5 由于双向关联会破坏分层,因此用查询来代替它

第16章 大型结构 - 图7 图16-6 初次对模型进行分层

虽然作业层与能力层的区别使这张图看上去很清楚了,但次序仍需要进一步细化。经过几个星期的实验之后,团队将注意力集中在另一个特性上。在很大程度上,最初的两个层主要考虑的是当前的情况或计划。但Router(以及其他很多未在图中画出的元素)并不是当前的作业或计划的一部分。它是用来帮助修改这些计划的。因此团队定义了一个新的层,让它来负责决策支持(Decision Support)。

“决策支持”职责层

软件的这个层为用户提供了用于制定计划和决策的工具,它具有自动制定一些决策的潜能(例如,当运输时间表发生变动时,自动重新制定运送Cargo的路线)。

Router是一个SERVICE,能帮助预订代理(booking agent)选择运送货物的最佳路线。因此Router明显属于决策支持层。

现在模型中的元素基本上都按照这3个层来组织了,唯一例外的是Transport Leg的“is preferred”属性。这个属性存在的原因是公司希望在可能的情况下优先使用自己的货轮,或者是那些签订了优惠合同的公司的货轮。is preferred属性用于使Router优先选择这些首选的运输工具。这个属性与“能力层”毫无关系。它是一个用于指导决策制定的策略。为了使用新的RESPONSIBILITY LAYER,需要对模型进行重构。

第16章 大型结构 - 图8 图16-7 对模型进行重构,使之符合新的分层结构

这次重构使Route Bias Policy变得更清楚,同时使得Transport Leg更专注于运输能力的基本概念。基于对领域的深刻理解而发现的大比例结构总是能够使模型更清楚地表达其含义。

现在,这个新模型更加符合大比例结构了。如图16-8所示。

开发人员在熟悉了选定的分层结构后,很容易区分出各个部分的角色和依赖关系。大比例结构的价值随着复杂度的增加而增加。

注意,虽然我使用了一个修改后的UML图来演示这个例子,但这只是为了表示分层而使用的一种方式。UML中并没有这种表示法,因此这些是作为额外的信息加上去的,目的是让读者看得更清楚。如果在你的项目中,代码就是最终的设计文档,那么最好可以使用一种可以按层查看类(或至少按照层来报告与这些类有关的信息)的工具。

第16章 大型结构 - 图9 图16-8 重构后的模型

大比例结构如何影响后续设计

一旦采用了一种大比例结构,后续的建模和设计决策就必须要把它考虑在内。为了说明这一点,假设我们必须在这个已分层的设计中增加一个新特性。领域专家们刚刚告诉我们一些针对特定类别危险品的航线约束。有些危险品在某些货轮或港口上是禁止装载的。我们必须使Router遵守这些规则。

有很多可行的方法。在未使用大比例结构时,一种吸引人的设计方法是让拥有Route Specification和Hazardous Material(HazMat)代码的对象负责把这些航线规则加进来,这个对象就是Cargo。

第16章 大型结构 - 图10 图16-9 用于制定危险货物运送路线的一种可能的设计

第16章 大型结构 - 图11 图16-10

问题是这种设计并不适合大比例结构。HazMat Route Policy Service并没有问题,它非常适合承担决策支持层的职责。问题在于Cargo(一个作业层对象)对HazMat Route Policy Service(一个决策支持层对象)的依赖上。只要项目还采用目前的分层,就不能使用这个模型,因为开发人员会认为设计将遵循分层结构,而这种依赖会使开发人员感到糊涂。

可能的设计选择总会有很多,这里我们只选择另外一种设计,这种设计符合大比例结构的规则。HazMat Route Policy服务本身是完全没有问题的,但我们需要把使用它的职责转移到别处。让我们尝试让Router来承担在搜索航线之前收集相关规则的职责。这意味着要修改Router接口,把规则可能依赖的那些对象包括进来。下面就是一种可能的设计,如图16-11所示。

第16章 大型结构 - 图12 图16-11 符合分层结构的一种设计

一种典型的交互如图16-12所示。

现在的这个设计并不一定就比前面那个设计更好。二者都是各有利弊。但如果项目的所有人员都采用一致的方式来制定决策,那么整体的设计就更容易理解,因此这也值得在细小的设计选择上做出一些适度的折中。

第16章 大型结构 - 图13 图16-12

如果所采用的结构强制性地要求我们做出很多别扭的设计选择,那么就要遵循EVOLVING ORDER(演变的顺序),在项目进行过程中评估这种结构,并修改甚至放弃它。

选择适当的层

要想找到一种适当的RESPONSIBILITY LAYER或大比例结构,需要理解问题领域并反复进行实验。如果遵循EVOLVING ORDER,那么最初的起点并不是十分重要,尽管差劲的选择确实会加大工作量。结构可能最后演变得面目全非。因此,下面将给出一些指导方针,无论是刚开始选择一种结构,还是对已有结构进行转换,这些指导方针都适用。

当对层进行删除、合并、拆分和重新定义等操作时,应寻找并保留以下一些有用的特征。

场景描述。层应该能够表达出领域的基本现实或优先级。选择一种大比例结构与其说是一种技术决策,不如说是一种业务建模决策。层应该显示出业务的优先级。

概念依赖性。“较高”层概念的意义应该依赖“较低”层,而低层概念的意义应该独立于较高的层。

CONCEPTUAL CONTOUR。如果不同层的对象必须具有不同的变化频率或原因,那么层应该能够容许它们之间的变化。

在为每个新模型定义层时不一定总要从头开始。在一系列相关领域中,有些层是固定的。

例如,在那些利用大型固定资产进行运作的企业(如工厂或货运)中,物流软件通常可以被组织为“潜能”层(上面例子中的“能力”层的另外一个名称)和“作业”层。

潜能层。我们能够做什么?潜能层不关心我们打算做什么,而关心能够做什么。企业的资源(包括人力资源)以及这些资源的组织方式是潜能层的核心。与供应商签订的合同也明确界定了企业的潜能。这个层几乎存在于任何业务领域中,但在那些相对来说依靠大型固定资产来支持业务运作的企业中(如运输和制造业)尤其突出。潜能也包括临时性的资产,但主要依赖临时资产来运作的企业可能会强调临时资产的层(这个层在例子中被称为“Capability”),这一点稍后会讨论。

作业层。我们正在做什么?我们利用这些潜能做了什么事情?像潜能层一样,这个层也应该反映出现实状况,而不是我们设想的状况。我们希望在这个层中看到自己的工作和活动:我们正在销售什么,而不是能够销售什么。通常来说,作业层对象可以引用潜能层对象,它甚至可以由潜能层对象组成,但潜能层对象不应该引用作业层对象。

在这类领域很多(也许是大部分)现有的系统中,这两个层可以涵盖一切对象(尽管可能会有某种完全不同的和更清晰的分解结构)。它们可以跟踪当前状况和正在执行的作业计划,以及问题报告或相关文档。但跟踪往往是不够的。当项目要为用户提供指导或帮助或者要自动制定一些决策时,就需要有另外一组职责,这些职责可以被组织到作业层之上的决策支持层中。

决策支持层。应该采取什么行动或制定什么策略?这个层是用来作出分析和制定决策的。它根据来自较低层(如潜能层或作业层)的信息进行分析。决策支持软件可以利用历史信息来主动寻找适用于当前和未来作业的机会。

决策支持系统对其他层(如作业层或潜能层)有概念上的依赖性,因为决策并不是凭空制定的。很多项目都利用数据仓库技术来实现决策支持。在这样的项目中,决策支持层实际上变成了一个独特的BOUNDED CONTEXT,并且与作业软件具有一种CUSTOMER/SUPPLIER关系。在其他项目中,决策支持层被更深地集成到系统中,就像前面的扩展示例讲到的那样。分层结构的一个内在的优点是较低的层可以独立于较高的层存在。这样有利于在较老的作业系统上分阶段引入新功能或开发高层次的增强功能。

另一种情形是软件实施了详细的业务规则或法律需求,这些规则或需求可以形成一个RESPONSIBILITY LAYER。

策略层。规则和目标是什么?规则和目标主要是被动的,但它们约束着其他层的行为。这些交互的设计是一个微妙的问题。有时策略会作为一个参数传给较低层的方法。有时会使用STRATEGY模式。策略层与决策支持层能够进行很好的协作,决策支持层提供了用于搜索策略层所设定的目标的方式,这些目标又受到策略层所设定的规则的约束。

策略层可以和其他层使用同一种语言来编写,但它们有时是使用规则引擎来实现的。这并不是说一定要把它们放到一个单独的BOUNDED CONTEXT中。实际上,通过在两种不同的实现技术中严格使用同一个模型,可以减小在这两种实现技术之间进行协调的难度。当规则与它们所应用的对象是基于不同模型编写的时候,要么复杂度会大大增加,要么对象会变得十分笨拙而难以管理。如图16-13所示。

第16章 大型结构 - 图14 图16-13 工厂自动化系统中的概念依赖性和切合点

很多企业并不是依靠工厂和设备能力来运营的。举两个例子,在金融服务或保险业中,潜能在很大程度上是由当前的运营状况决定的。一家保险公司在考虑签保单承担理赔责任时,要根据当前业务的多样性来判断是否有能力承担它所带来的风险。潜能层有可能会被合并到作业层中,这样就会演变出一种不同的分层结构。

这些情况下经常出现的一个层是对客户所做出的承诺(见图16-14)。

承诺层。我们承诺了什么?这个层具有策略层的性质,因为它表述了一些指导未来运营的目标;但它也有作业层的性质,因为承诺是作为后续业务活动的一部分而出现和变化的。

潜能层和承诺层并不是互相排斥的。在有的领域中(如一家提供很多定制运输服务的运输公司),这两个层都很重要,因此可以同时使用它们。与这些领域密切相关的其他层也会用到。我们需要对分层结构进行调整和实验,但一定要使分层系统保持简单,如果层数超过4或5,就比较难处理了。层数过多将无法有效地描述领域,而且本来要使用大比例结构解决的复杂性问题又会以一种新的方式出现。我们必须对大比例结构进行严格的精简。

第16章 大型结构 - 图15 图16-14 投资银行系统中的概念依赖性和切合点

虽然这5个层对很多企业系统都适用,但并不是所有领域的主要概念都涵盖在这5个层中。有些情况下,在设计中生硬地套用这种形式反而会起反作用,而使用一组更自然的RESPONSIBILITY LAYER会更有效。如果一个领域与上述讨论毫无关系,所有的分层可能都必须从头开始。最后,我们必须根据直觉选择一个起点,然后通过EVOLVING ORDER来改进它。

16.4 模式:KNOWLEDGE LEVEL

第16章 大型结构 - 图16

“KNOWLEDGE LEVEL是”一组描述了另一组对象应该有哪些行为的对象。

[Martin Fowler,“Accountability”,www.martinfowler.com]

当我们需要让用户对模型的一部分有所控制,而模型又必须满足更大的一组规则时,可以利用KNOWLEDGE LEVEL(知识级别)来处理这种情况。它可以使软件具有可配臵的行为,其中实体中的角色和关系必须在安装时(甚至在运行时)进行修改。

在《分析模式》[Fowler 1996,pp.24–27]一书中,知识级别这种模式是讨论在组织内部对责任进行建模的时候提到的,后来在会计系统的过账规则中也用到了这种模式。虽然有几章内容涉及此模式,但并没有为它单独开一章,因为它与书中所讨论的大部分模式都不相同。KNOWLEDGE LEVEL并不像其他分析模式那样对领域进行建模,而是用来构造模型的。

为了使问题更具体,我们来考虑一下“责任”(accountability)模型。组织是由人和一些更小的组织构成的,并且定义了他们所承担的角色和互相之间的关系。不同的组织用于控制这些角色和关系的规则大不相同。有的公司分为各个“部门”,每个部门可能由一位“主管”来领导,他要向“副总裁”汇报。而有的公司则分为各个“模块”(module),每个模块由一位“经理”来领导,他要向“高级经理”汇报。还有一些组织采用的是“矩阵”形式,其中每个人都出于不同的目的而向不同的经理汇报。

一般的应用程序都会做一些假设。当这些假设并不恰当时,用户就会在数据录入字段中输入与预期不符的数据。由于语义被用户改变,因此应用程序的任何行为都可能会失败。用户将会想出一些迂回的办法来执行这些行为,或者关闭一些高级特性。他们不得不费力地找出他们的操作与软件行为之间的复杂对应关系。这样他们永远也得不到良好的服务。

当必须要对系统进行修改或替换时,开发人员(或迟或早)会发现,有一些功能的真实含义并不像它们看上去的那样。它们在不同的用户社区或不同情况下具有完全不同的含义。在不破坏这些互相叠加的含义的前提下修改任何东西都是非常困难的。要想把数据迁移到一个“更合适”的系统中,必须要理解这些奇怪的部分,并对其进行编码。

示例 员工工资和养老金系统,第1部分

一家中等规模公司的人力资源部门有一个用于计算工资和养老金代扣的简单程序。如图16-15和图16-16所示。

第16章 大型结构 - 图17 图16-15 原来的模型,在新的需求下被过多地约束

但现在,管理层决定办公室行政人员应该进入“固定受益”(Defined Benefit)退休计划。问题在于办公室行政人员是按小时付薪酬的,而这个模型不支持混合计算。因此必须修改模型。

第16章 大型结构 - 图18 图16-16 用原来的模型表示出来的一些员工

下面的模型提议非常简单,只是把约束去掉了,如图16-17所示。但也会出现一些错误,如图16-18所示。

第16章 大型结构 - 图19 图16-17 提议的模型,现在的情况是约束过少了

第16章 大型结构 - 图20 图16-18 员工可能会与错误的计划关联起来

在这个模型中,每个员工随便加入哪一种退休计划都可以,因此每位办公室行政人员都可以改变退休计划。管理层最后放弃了这个模型,因为它没有反映出公司的策略。一些行政人员可以选择“固定受益”计划,而另外一些则不能。要是使用这个模型,连门卫也可以改变退休计划。管理层需要一个能够实施以下策略的模型:

办公室行政人员按小时付薪酬,且采用固定受益退休计划。

这个策略暗示出job title(工作头衔)字段现在表示了一个重要的领域概念。开发人员可以重构模型,用Employee Type(员工类型)把这个概念明确显示出来,如图16-19和图16-20所示。

第16章 大型结构 - 图21 图16-19 Type对象能够满足需求

第16章 大型结构 - 图22 图16-20 每个Employee Type被指定一个Retirement Plan

需求可以像下面这样用UBIQUITOUS LANGUAGE来表述出来:

一个EMPLOYEE TYPE可以被指定两种RETIREMENT PLAN中的任何一种,也可以被指定两种工资中的任何一种。

EMPLOYEE受EMPLOYEE TYPE约束。

只有superuser(超级用户)才能编辑Employee Type对象,而且只有当公司策略变更时,他才能修改此对象。人事部门的普通用户只能修改Employee对象,或只能将这些对象指定为另一种Employee Type。

这种模型可以满足需求。开发人员认识到了一两个隐含的概念,但这只是灵机一动才想到的。他们并没有具体的思路可供追查下去,因此他们暂时结束了这一天的工作。

静态模型可能引起问题。但在一个过于灵活的系统中,如果任何可能的关系都允许存在,问题一样糟糕。这样的系统使用起来会很不方便,而且会导致组织无法实施自己的规则。

让每个组织完全定制自己的软件也是不现实的,即使组织能够担负得起定制软件的费用,组织结构也可能会频繁变化。

因此,这样的软件必须为用户提供配臵选项,以便反映出组织的当前结构。问题是,在模型对象中添加这些选项会使这些对象变得难于处理。要求的灵活性越高,模型就会变得越复杂。

如果在一个应用程序中,ENTITY的角色和它们之间的关系在不同的情况下有很大变化,那么复杂性会显著增加。在这种情况下,无论是一般的模型还是高度定制的模型,都无法满足用户的需求。为了兼顾各种不同的情形,对象需要引用其他的类型,或者需要具备一些在不同情况下包括不同使用方式的属性。具有相同数据和行为的类可能会大量增加,而这些类的唯一作用只是为了满足不同的组装规则。

在我们的模型中嵌入了另一个模型,而它的作用只是描述我们的模型。KNOWLEDGE LEVEL分离了模型的这个自我定义的方面,并清楚地显示了它的限制。

KNOWLEDGE LEVEL是REFLECTION(反射)模式在领域层中的一种应用,很多软件架构和技术基础设施中都使用了它,[Buschmann et al.1996])中给出了详尽介绍。REFLECTION模式能够使软件具有―自我感知‖的特性,并使所选中的结构和行为可以接受调整和修改,从而满足变化需要。这是通过将软件分为两个层来实现的,一个层是―基础级别‖(base level),它承担应用程序的操作职责;另一个是―元级别‖(meta level),它表示有关软件结构和行为方面的知识。

值得注意的是,我们并没有把这种模式叫做知识―层‖(layer)。虽然REFLECTION与分层很类似,但反射却包含双向依赖关系。

Java有一些最基本的内臵REFLECTION机制,它们采用的是协议的形式,用于查询一个类的方法等。这样的机制允许用户查询有关它自己的一些设计信息。CORBA也有一些扩展(但类似)的REFLECTION协议。一些持久化技术增加了更丰富的自描述特性,在数据表与对象之间提供了部分自动化的映射。还有其他一些技术例子。这种模式也可以在领域层中使用。

KNOWLEDGE LEVEL与REFLECTION所使用的术语比较 第16章 大型结构 - 图23 ①POSA是Pattern-Oriented Software Architecture[Buschmann et al.1996]一书的缩写。

要明确的一点是,编程语言的反射工具并不是用于实现领域模型的KNOWLEDGE LEVEL的。这些元对象描述的是语言构造本身的结构和行为。相反,KNOWLEDGE LEVEL必须使用普通对象来构造。

KNOWLEDGE LEVEL具有两个很有用的特性。首先,它关注的是应用领域,这一点与人们所熟悉的REFLECTION模式的应用正好相反。其次,它并不追求完全的通用性。正如一个SPECIFICATION可能比通用的断言更有用一样,专门为一组对象和它们的关系定制的一个约束集可能比一个通用的框架更有用。KNOWLEDGE LEVEL显得更简单,而且可以传达设计者的特别意图。

因此:

创建一组不同的对象,用它们来描述和约束基本模型的结构和行为。把这些对象分为两个“级别”,一个是非常具体的级别,另一个级别则提供了一些可供用户或超级用户定制的规则和知识。

像所有有用的思想一样,REFLECTION和KNOWLEDGE LEVEL可能令人们感到振奋,但不应滥用这种模式。它确实能够使对象不必为了满足各种不同情形下的需求而变得过于复杂,但它所引入的间接性也会使系统变得更模糊。如果KNOWLEDGE LEVEL太复杂,开发人员和用户就很难理解系统的行为。负责配臵它的用户(或超级用户)最终将需要具备程序员的技能,甚至需要掌握处理元数据的技能。如果他们出现了错误,应用程序也将会产生错误行为。

而且,数据迁移的基本问题并没有完全得到解决。当KNOWLEDGE LEVEL中的某个结构发生变化时,必须对现有的操作级别中的对象进行相应的处理。新旧对象确实可以共存,但无论如何都需要进行仔细的分析。

所有这些问题为KNOWLEDGE LEVEL的设计人员增加了一个沉重的负担。设计必须足够健壮,因为不仅要解决开发中可能出现的各种问题,而且还要考虑到将来用户在配臵软件时可能会出现的各种问题。如果得到合理的运用,KNOWLEDGE LEVEL能够解决一些其他方式很难解决的问题。如果系统中某些部分的定制非常关键,而要是不提供定制能力就会破坏掉整个设计,这时就可以利用知识级别来解决这一问题。

示例 员工工资和养老金系统,第2部分:KNOWLEDGE LEVEL

我们的团队成员又回来了,经过了一夜的休息,他们恢复了精神,团队中的一个人对系统中一个难处理的问题有了点思路。为什么有些对象要被限制起来,而其他对象则可以自由编辑呢?那些受限制的对象让他想到了KNOWLEDGE LEVEL模式,他决定尝试着从这个角度来观察一下模型,才发现本来就可以用这种方式来观察模型的。

从图16-21可以看出,受限制的对象都在KNOWLEDGE LEVEL中,而可以自由编辑的对象都在操作级别中,区分得非常清楚。虚线上面的所有对象描述了类型或长期策略。Employee Type有效地把行为加在Employee上。

这位开发人员把他的想法告诉了大家,这使另一个人又产生了另一个想法。按照KNOWLEDGE LEVEL对模型进行组织后,模型变得更清晰了,这使她一下子发现了昨天困扰她的那个问题——两个完全不同的概念被合并到同一个模型中。昨天她在团队讨论所使用的语言中就听到了这个问题,只是没有注意到而已:

第16章 大型结构 - 图24 图16-21 从现有模型中识别出隐含的KNOWLEDGE LEVEL

一个Employee Type可以被指定两种Retirement Plan中的任何一种,也可以被指定两种工资中的任何一种。

但这实际上并不是用UBIQUITOUS LANGUAGE中来表达的声明。模型中并没有“payroll”(工资)。他们只是根据自己的需要来讲话,而没有使用实际就有的通用语言。payroll的概念在模型中是隐含的,与Employee Type混在一起。在分离出KNOWLEDGE LEVEL以前,它并不明显,而且这个声明中的所有元素都出现在同一个级别上,只有一个元素例外。

根据这种理解,她重构了一个真正支持该声明的模型。

为了让用户控制那些制约对象之间关联的规则,开发团队开发了一个包含隐含KNOWLEDGE LEVEL的模型。

第16章 大型结构 - 图25 图16-22 Payroll现在已经显示出来了,它已与Employee Type分离

第16章 大型结构 - 图26 图16-23 每个Employee Type现在都有一个Retirement Plan和一个Payroll

特有的访问约束和一种“事物—事物”型的关系对开发团队起到了提示的作用,使他们看出了隐含的KNOWLEDGE LEVEL。一旦KNOWLEDGE LEVEL被分离出来,它就能够使模型变得非常清晰,从而可以通过提取出Payroll将两个重要的领域概念分开。

像其他大比例结构一样,KNOWLEDGE LEVEL也不是必须要使用的。没有它,对象照样能工作,而且团队可能仍能够认识到他们需要将Employee Type与Payroll分离。当项目进行到某个时刻,这种结构看起来已经没什么作用了,那么就可以放弃它。但现在它对于描述系统很有用,并且能够帮助开发人员理解模型。

乍看上去,KNOWLEDGE LEVEL像是RESPONSIBILITY LAYER(特别是策略层)的一个特例,但它并不是。首先,两个级别之间的依赖性是双向的,而在层次结构中,较低的层不依赖于较高的层。

实际上,RESPONSIBILITY LAYER可以与其他大部分的大比例结构共存,它提供了另一种用来组织模型的维度。

16.5 模式: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不适合作为项目的第一个大比例结构,也不适合作为第二个。最成功的例子都是在完全开发出了多个专门应用之后才采用这种结构的。

示例 SEMATECH CIM 框架

在一家生产计算机芯片的工厂中,一组一组的硅片(称为lot)从一台机器传送到另一台机器,通过上百道加工工序,直到印刷上微电路并完成蚀刻。工厂需要一个软件来跟踪每个lot,记录下来它上面已经完成的加工,然后指挥工人或自动设备把它送到下一台正确的机器上,并进行下一次正确的加工。这样的软件称为制造执行系统(Manufacturing Execution System,MES)。

工厂使用了数十家供应商生产的数百台不同的机器,每道工序都仔细设计了定制的配方。为这个复杂的混合加工过程开发MES软件是一项异常艰巨的任务,而且费用也十分高昂。为了解决这些问题,SEMATECH(一家行业协会)开发了CIM框架。

CIM框架庞大而复杂,它有很多方面,但只有两个方面与我们这里的讨论相关。首先,这个框架为半导体MES领域的基本概念定义了抽象接口,换言之,以ABSTRACT CORE的形式定义了CORE DOMAIN。这些接口定义既包括行为上的,也包括语义上的。

第16章 大型结构 - 图27 图16-24 高度简化的CIM接口子集,提供了一些实现样例

如果某家供应商生产了一种新的机器,他们必须开发Process Machine接口的一个专用实现。只要他们遵守该接口,他们的机器控制组件就可以插入到任何基于CIM框架的应用程序中。

在定义了这些接口之后,SEMATECH又定义了组件在应用程序中进行交互时需要遵守的规则。任何基于CIM框架的应用程序都必须实现一个协议,通过这个协议来为那些已经实现部分接口的对象提供服务。如果这个协议已经实现,而且应用程序严格遵守抽象接口,那么这个应用程序就可以使用这些接口所提供的服务,而不用管它们是如何实现的。这些接口以及为了使用接口而实现的协议组合在一起,构成了具有严格限制的大比例结构。

第16章 大型结构 - 图28 图16-25 用户把一个lot放到下一台机器上,并把这次操作记录到计算机中

这个框架需要使用专门的基础设施。它主要使用CORBA来提供持久化、事务、事件和其他技术服务。但它的PLUGGABLE COMPONENT FRAMEWORK的定义很有趣,它允许人们独立开发软件,并把开发出来的软件平滑地集成到庞大的系统中。没有人会知道这个系统中的所有细节,但每个人都理解整体视图。

数千人是如何分工来制作一个由40 000多块组成的“艾滋病纪念拼被”的?

几条简单的规则为“艾滋病拼图被子”提供了一种大比例结构,而细节则由各个志愿者来完成。注意规则重点关注的3个方面,一是整体任务(纪念那些因艾滋病而死去的人们),二是各个小块所具有的那些使其容易拼到整体中的特性,三是处理更大的块的能力(如把它折叠起来)。

以下就是艾滋病纪念拼被的一个拼块的制作方法

[摘自艾滋病纪念拼被网站,www.aidsquilt.org]

设计拼块

把要纪念的人的名字写到拼块上。可以自由加入其他一些信息,如出生、死亡日期和出生地等,每个拼块仅限一人……

选择你的材料

记住,被单要被折叠和打开许多次,因此材料的耐久性很重要。由于胶会随着时间失效,因此最好把东西缝到拼块上。最好使用重量适中、不具有拉伸性的布料,如棉帆布或毛葛。

设计可以采用横向或纵向,但最终镶好边的拼块必须是3英尺×6英尺(90 cm×180 cm)——不能多也不能少!裁剪布料时,每个边留出2~3英寸的镶边。如果你自己不能镶边,我们会为你代劳。无需为拼块缝制夹层,但建议在背面缝一个衬垫,这样当把拼块放到地上时,可以保持干净,也有助于保持布料不变形。

制作拼块

制作拼块时可能会用到以下技术。

缝饰:在背景布料上缝上其他的织物、信件或小的纪念品。不要使用胶水,因为它很容易失效。

用颜料涂色:刷上纺织颜料或快速上色染料,也可以使用不褪色的墨水笔。不要使用“棉花彩”[6],因为它的黏性太大了。

模绘:用铅笔把你的设计画到布料上,然后把得到的模板垫高,再用刷子涂上纺织颜料或不褪色的标记。

拼贴:在拼块上使用的材料一定不要把布料划破(因此不要使用玻璃和金属片),还要注意不要使用体积很大的物品。

照片:加照片或信件的最好方法是把它们影印到烫印转印纸(iron-on transfer)上,再由烫印转印纸印到100%的纯棉布料上,再把这块布料缝到拼块上。也可以用乙烯材料把照片塑封起来,再缝到拼块上(不要放在中央,以避免折叠)。

16.6 结构应该有一种什么样的约束

本章所讨论的大比例结构很广泛,从非常宽松的SYSTEM METAPHOR到严格的PLUGGABLE COMPONENT FRAMEWORK。当然,还有很多其他结构,而且,甚至在一个通用的结构模式中,在制定规则上也可以选择多种不同的严格程度。

例如,RESPONSIBILITY LAYER规定了一种用于划分模型概念以及它们的依赖性的方式,但我们也可以添加一些规则,来指定各个层之间的通信模式。

假设有一家制造厂,每个零件在哪台机器上加工(根据工艺配方)完全由软件来指挥。正确的加工命令是从策略层发出的,并在作业层执行。但工厂的实际生产不可避免地会有错误。实际情况将与软件的规则不符。现在,作业层必须要反映出工厂的实际情况,这意味着当一个零件偶然被放到一台错误的机器上时,机器必须无条件地接受它。这种异常情况需要以某种方式传递到更高的层。然后,决策制定层可以利用其他策略来纠正这种情况,可以把该零件重新送到修理流程或直接丢弃它。但作业层不知道较高层的任何信息。通信必须是单向的,不能让较低层产生对较高层的依赖性。

通常,这种信号传递是通过某种事件机制实现的。每当作业层对象的状态发生变化时,它们就将生成事件。策略层对象将监听来自较低层的相关事件。如果一个事件违反了某个规则,该规则将执行一个动作(规则定义的一部分)来给出适当的响应,或者生成一个事件反馈给更高的层,以便帮助更高的层做出决策。

例如,在银行中,当投资组合中的某些部分发生变动时,资产的价值会发生改变(作业层)。当这些值超过投资组合的配臵限制时(策略层),交易商可能就会接到通知,然后他可以通过买入或卖出资产来恢复平衡。

我们可以为每种不同的情况设计不同的事件机制,也可以让特殊层中的对象在交互时遵守一种一致的模式。结构越严格,一致性就越高,设计也越容易理解。如果结构适当的话,规则将推动开发人员得出好的设计。不同的部分之间会更协调。

另一方面,约束也会限制开发人员所需的灵活性。在异构系统中,特别是当系统使用了不同的实现技术时,可能无法跨越不同的BOUNDED CONTEXT来使用非常特殊的通信路径。

因此一定要克制,不要滥用框架和死板地实现大比例结构。大比例结构的最重要的贡献在于它具有概念上的一致性,并帮助我们更深入地理解领域。每条结构规则都应该使开发变得更容易实现。

16.7 通过重构得到更适当的结构

在当今这个时代,软件开发行业正在努力摆脱过多的预先设计,因此一些人会把大比例结构看作是倒退回了过去那段使用瀑布架构的令人痛苦的年代。但实际上,只有深入地理解领域和问题才能发现一种非常有用的结构,而获得这种深刻的理解的有效方式就是迭代开发过程。

团队要想坚持EVOLVING ORDER原则,必须在项目的整个生命周期中大胆地反复思考大比例结构。团队不应该一成不变地使用早期构思出来的那个结构,因为那时所有人对领域或需求的理解都不够完善。

遗憾的是,这种演变意味着最终的结构不会在项目一开始就被发现,而且我们必须在开发过程中进行重构,以便得到最终的结构。这可能很难实现,而且需要高昂的代价,但这样做是非常必要的。有一些通用的方法可以帮助控制成本并最大化收益。

16.7.1 最小化

控制成本的一个关键是保持一种简单、轻量级的结构。不要试图使结构面面俱到。只需解决最主要的问题即可,其他问题可以留到后面一个一个地解决。

开始最好选择一种松散的结构,如SYSTEM METAPHOR或几个RESPONSIBILITY LAYER。不管怎样,一种最小化的松散结构可以起到轻量级的指导作用,它有助于避免混乱。

16.7.2 沟通和自律

整个团队在新的开发和重构中必须遵守结构。要做到这一点,整个团队必须理解这种结构。必须把术语和关系纳入到UBIQUITOUS LANGUAGE中。

大比例结构为项目提供了一个术语表,它概要地描述了整个系统,并且使不同人员能够做出一致的决策。但由于大多数大比例结构只是松散的概念指导,因此团队必须要自觉地遵守它。

如果很多人不遵守结构,它慢慢就会失去作用。这时,结构与模型和实现的各个部分之间的关系无法总是在代码中明确地反映出来,而且功能测试也不再依赖结构了。此外,结构往往是抽象的,因此很难保证在一个大的团队(或多个团队)中一致地应用它。

在大多数团队中,仅仅通过沟通是不足以保证在系统中采用一致的大比例结构的。至关重要的一点是要把它合并到项目的通用语言中,并让每个人都严格地使用UBIQUITOUS LANGUAGE。

16.7.3 通过重构得到柔性设计

其次,对结构的任何修改都可能导致大量的重构工作出现。随着系统复杂度的增加和人们理解的加深,结构会不断演变。每次修改结构时,必须修改整个系统,以便遵守新的秩序。显然这需要付出大量工作。

但这并不像听上去那么糟糕。根据我的观察,采用了大比例结构的设计往往比那些未采用的设计更容易转换。即使是从一种结构更改为另一种结构(例如,从METAPHOR改为LAYER)也是如此。我无法完全解释清楚这是什么原因。部分原因是当完全理解了某个系统的当前布局之后,再重新安排它就会更容易,而且先前的结构使得重新布局变得更容易。还有部分原因是用于维护先前结构的那种自律性已经渗透到了系统的各个方面。但我觉得还有更多的原因,因为当一个系统先前已经使用了两种结构时,它的更改甚至更加容易。

一件新皮茄克穿起来又硬又不舒服,但穿了一天之后,肘部经过若干次弯曲后就会变得更容易弯曲。再穿几天之后,肩部也会变得宽松,茄克也更容易穿上了。几个月后,皮质开始变得柔软,穿着会更舒适,也更容易穿上。同样,对模型反复进行合理的转换也有相同效果。不断增加的知识被合并到模型中,更改的要点已经被识别出来,并且更改也变得更灵活,同时模型中一些稳定的部分也得到了简化。这样,底层领域的更显著的CONCEPTUAL CONTOUR就会在模型结构中浮现出来。

16.7.4 通过精炼可以减轻负担

对模型施加的另一项关键工作是持续精炼。这可以从各个方面减小修改结构的难度。首先,从CORE DOMAIN中去掉一些机制、GENERIC SUBDOMAIN和其他支持结构,需要重构的内容就少多了。

如果可能的话,应该把这些支持元素简单地定义成符合大比例结构的形式。例如,在一个RESPONSIBILITY LAYER系统中,可以把GENERIC SUBDOMAIN定义成只适合放到某个特定层中。当使用了PLUGGABLE COMPONENT FRAMEWORK的时候,可以把GENERIC SUBDOMAIN定义成完全由某个组件拥有,也可以定义成一个SHARED KERNEL,供一组相关组件使用。这些支持元素可能需要进行重构,以便找到它们在结构中的适当位臵,但它们的移动与CORE DOMAIN是独立的,而且移动也限制在很小的范围内,因此更容易实现。最后,它们都是次要元素,因此它们的精化不会影响大局。

通过精炼和重构得到更深层理解的原理甚至也适用于大比例结构本身。例如,最初可以根据对领域的初步理解来选择分层结构,然后逐步用更深层次的抽象(这些抽象表达了系统的基本职责)来代替它们。这种极高的清晰度使人们能够透彻地理解领域,这也是我们的目标。它也是一种使系统的整体控制变得更容易、更安全的手段。