第12章 将设计模式应用于模型

到目前为止,本书所探讨的模式都是专门用来在MODEL-DRIVEN DESIGN的上下文中解决领域模型的问题。但实际上,大部分已发布的模式都更侧重于解决技术问题。设计模式与领域模式之间有什么区别?《设计模式》这部经典著作的作者为初学者指出了以下事实[Gamma et al.1995,p.3]:

立场不同会影响人们如何看待什么是模式以及什么不是模式。一个人所认为的模式在另一个人看来可能是基本构造块。本书将在一定的抽象层次上讨论模式。设计模式并不是指像链表和散列表那样可以被封装到类中并供人们直接重用的设计,也不是用于整个应用程序或子系统的复杂的、领域特定的设计。本书中的设计模式是对一些交互的对象和类的描述,我们通过定制这些对象和类来解决特定上下文中的一般设计问题。

在《设计模式》中,有些(但并非所有)模式可用作领域模式,但在这样使用的时候,需要变换一下重点。《设计模式》中的设计模式把相关设计元素归为一类,这些元素能够解决在各种上下文中经常遇到的问题。这些模式的动机以及模式本身都是从纯技术角度描述的。但这些元素中的一部分在更广泛的领域和设计上下文中也适用,因为这些元素所对应的基本概念在很多领域中都会出现。

除了《设计模式》中介绍的模式以外,近年来还出现了其他很多技术设计模式。有些模式反映了在一些领域中出现的深层概念。这些模式都有很大的利用价值。为了在领域驱动设计中充分利用这些模式,我们必须同时从两个角度看待它们:从代码的角度来看它们是技术设计模式,从模型的角度来看它们就是概念模式。

我们将把《设计模式》所介绍的特定模式作为样例,来说明如何将人们所认为的设计模式应用到领域模型中,而且这个例子还将澄清技术设计模式与领域模式之间的区别。本章还将通过COMPOSITE(组合)和STRATEGY(策略)这两种模式演示如何通过改变思考方式,用一些经典的设计模式来解决领域问题。

12.1 模式:STRATEGY(也称为POLICY)

第12章 将设计模式应用于模型 - 图1

定义了一组算法,将每个算法封装起来,并使它们可以互换。STRATEGY允许算法独立于使用它的客户而变化。[Gamma et al.1995]

领域模型包含一些并非用于解决技术问题的过程,将它们包含进来是因为它们对处理问题领域具有实际的价值。当必须从多个过程中进行选择时,选择的复杂性再加上多个过程本身的复杂性会使局面失去控制。

当对过程进行建模时,我们经常会发现过程有不止一种合理的实现方式,而如果把所有的可选项都写到过程的定义中,定义就会变得臃肿而复杂,而且可供我们选择的实际行为也会因为混杂在其他行为中而显得模糊不清。

我们希望把这些选择从过程的主体概念中分离出来,这样既能够看清主体概念,也能更清楚地看到这些选择。软件设计社区中众所周知的STRATEGY模式就是为了解决这个问题的,虽然它的侧重点在于技术方面。这里,我们把它当成模型中的一个概念来使用,并在该模型的代码实现中把它反映出来。我们同样也需要把过程中极易发生变化的部分与那些更稳定的部分分离开。

因此:

我们需要把过程中的易变部分提取到模型的一个单独的“策略”对象中。将规则与它所控制的行为区分开。按照STRATEGY设计模式来实现规则或可替换的过程。策略对象的多个版本表示了完成过程的不同方式。

通常,作为设计模式的STRATEGY侧重于替换不同算法的能力,而当其作为领域模式时,其侧重点则是表示概念的能力,这里的概念通常是指过程或策略规则。

示例 路线查找(Route-Finding)策略

我们把一个Route Specification(路线规格)传递给Routing Service(路线服务),Routing Service的职责是构造一个满足SPECIFICATION的详细的Itinerary。这个SERVICE是一个优化引擎,可以通过调节它来查找最快的路线或最便宜的路线。

第12章 将设计模式应用于模型 - 图2 图12-1 带选项的SERVICE接口需要条件逻辑

这种设臵看上去似乎没问题,但仔细观察路线代码就会发现,每个计算中都有条件判断,判断最快还是最便宜的逻辑分散在程序各处。当为了做出更精细的航线选择而把新标准添加进来时,麻烦会更多。

解决此问题的一种方法是把这些起调节作用的参数分离到STRATEGY中。这样它们就可以被明确地表示出来,并作为参数传递给Routing Service。

现在,Routing Service就可以用一种完全相同的、无需进行条件判断的方式来处理所有请求了,它按照Leg Magnitude Policy(航段规模策略)的计算,找出一系列规模较小的Leg(航段)。

这种设计具有《设计模式》中所介绍的STRATEGY模式的优点。按这种思路设计的应用程序可以提供丰富的功能,同时也很灵活,现在,可以通过安装适当的Leg Magnitude Policy来控制和扩展Routing Service的行为。图12-2中显示的只是最明显的两种STRATEGY(最快或最便宜)。可能还会有一些在速度和成本之间进行权衡考虑的组合策略。也可以加进其他的因素,例如,在预订货物时优先选择公司自己的运输系统,而不是外包给其他运输公司。不使用STRATEGY模式同样能实现这些修改,但必须将逻辑添加到Routing Service的内部(这会是一个麻烦的过程),而且这些逻辑会使接口变得臃肿。解耦确实令Routing Service更清楚且易于测试。

第12章 将设计模式应用于模型 - 图3 图12-2 通过STRATEGY(或者POLICY)来确定选项(STRATEGY是作为参数传入的)

现在,领域中的一个至关重要的规则明确地显示出来了,也就是在构建Itinerary时用于选择Leg的基本规则。它传达了这样一个知识:路线选择的基础是航段的一个特定属性(有可能是派生属性),这个属性最后可归结为一个数字。这样,我们就能够通过领域语言很简单地定义Routing Service的行为:Routing Service根据所选的STRATEGY来选择Leg总规模最小的Itinerary。

说明:以上讨论暗示了一件事。Routing Service在查找Itinerary时实际上会计算Leg的规模。这种方法在概念上比较直接,而且可以生成一个合理的原型实现,但它的效率可能令人无法接受。第14章会再次讨论这个应用程序,其将使用相同的接口,但采用完全不同的Routing Service实现。

我们在领域层中使用技术设计模式时,必须认识到这样做的另外一种动机,也是它的另一层含义。当所使用的STRATEGY对应于某种实际的业务策略时,模式就不再仅仅是一种有用的实现技术了(但它在实现方面的价值并未改变)。

设计模式的结论也完全适用于领域层。例如,在《设计模式》一书中,Gamma等人指出客户必须知道不同的STRATEGY,这也是建模的一个关注点。如果单纯从实现上来考虑,使用策略可能会增加系统中对象的数目。如果这是个问题,可以把STRATEGY实现为无状态对象,以便在上下文中进行共享,从而减小开销。《设计模式》中对实现方法的全面讨论在这里也适用,这是因为我们仍然在使用STRATEGY,只是动机稍有不同,这会对我们的选择产生一些影响,,但设计模式中的经验仍然是可以借鉴的。

12.2 模式:COMPOSITE

第12章 将设计模式应用于模型 - 图4

将对象组织为树来表示部分—整体的层次结构。利用COMPOSITE,客户可以对单独的对象和对象组合进行同样的处理。[Gamma et al.1995]

在对复杂的领域进行建模时,我们经常会遇到由多个部分组成的重要对象,这些部分本身又由其他一些部分组成,依此类推,有时甚至会出现任意深度的嵌套。在一些领域中,各层嵌套在概念上是有区别的,但在另一些领域中,各个部分与它们所组成的整体是完全相同的事物,只是规模较小一些而已。

当嵌套容器的关联性没有在模型中反映出来时,公共行为必然会在层次结构的每一层重复出现,而且嵌套也变得僵化(例如,容器通常不能包含同一层中的其他容器,而且嵌套的层数也是固定的)。客户必须通过不同的接口来处理层次结构中的不同层,尽管这些层在概念上可能没有区别。通过层次结构来递归地收集信息也变得非常复杂。

当在领域中应用任何一种设计模式时,首先关注的问题应该是模式的意图是否确实适合领域概念。以递归的方式遍历一些相互关联对象确实比较方便,但它们是否真的存在整体—部分层次结构?你是否发现可以通过某种抽象方式把所有部分都归到同一概念类型中?如果你确实发现了这种抽象方式,那么使用COMPOSITE可以令模型的这些部分变得更清晰,同时使你能够借助设计模式所提供的那些经过深思熟虑的设计及实现的考量。

因此:

定义一个把COMPOSITE的所有成员都包含在内的抽象类型。在容器上实现那些查询信息的方法时,这些方法返回由容器内容所汇总的信息。而“叶”节点则基于它们自己的值来实现这些方法。客户只需使用抽象类型,而无需区分“叶”和容器。

相对而言,这是一种明显的结构层面上的模式,但设计人员通常不会主动地充实它的操作方面。COMPOSITE模式在每个结构层上都提供了相同的行为,而且无论是较小的部分还是较大的部分,都可以对这些部分提出一些有意义的问题,这些问题能够透明地反映出它们的构成情况。这种严格的对称是组合模式具有强大能力的关键所在。

示例 由Route构成的Shipment Route

完整的货物运输路线是很复杂的。首先,必须用卡车把集装箱运输到铁路终点站,然后运送到港口,之后用货轮运输到另一个港口,中间可能还会换船,最后还要进行地面运输才能到达目的地。

第12章 将设计模式应用于模型 - 图5 图12-3 由“leg”(航段)构成的“route”(航线)

一个应用开发团队创建了一个对象模型,它表示了一个航线可以由任意多个航段组成。

第12章 将设计模式应用于模型 - 图6 图12-4 Route的类图,其中Route由多个Leg组成

利用这个模型,开发人员可以根据预订请求来创建Route对象。他们可以把这些Leg组织为一步一步运输货物的操作计划。在这个过程中他们发现了一些问题。

开发人员原来一直认为航线是由任意多个航段组成的,而各个航段之间并没有什么区别。

第12章 将设计模式应用于模型 - 图7 图12-5 开发人员的航线概念

而事实上领域专家把航线看成是由5个逻辑段组成的序列。

第12章 将设计模式应用于模型 - 图8 图12-6 业务专家的航线概念

其他问题先不考虑,这些小段的航线可能是由不同的人在不同时间规划的,因此必须区别对待。通过更仔细的研究可以发现,“门航段”(door leg)与其他航段大不相同,它涉及在当地雇用卡车甚至是客户运输,这与详细计划的铁路和货船运输完全不同。

反映了所有这些区别的对象模型渐渐变得复杂起来。

第12章 将设计模式应用于模型 - 图9 图12-7 详细的Route类图

从结构上看这个模型并不是很差,但在操作计划的处理上失去了一致性,因此代码(甚至是行为的描述)变得复杂得多。其他复杂之处也渐渐显现。任何一条航线的遍历都涉及不同类型对象的多个集合。

运用COMPOSITE模式能使特定客户在不同层上都使用这种构造进行统一的处理,因为大的航线是由小段的航线构成的。这种视图在概念上也是合理的。每一层Route都是集装箱从一个地点到另一个地点的移动,最后都归结为一个独立的航段(参见图12-8)。

与前面那个类图不同,从现在这个静态类图看不出来门航段是如何与其他航段组合在一起的。但模型并不只包含静态类图。我们将通过其他的图(参见图12-9)和代码(现在代码简单多了)来表示这些航段的组合信息。这个模型抓住了所有这些不同类型Route的深层关联性。生成操作计划的工作再次变得简单了,而且其他路线遍历操作也变得简单了。

利用这种“由航线组成航线”的方法,我们可以把各个航线的端点连接到一起来得到从一个地点到另一个地点的航线,从而可以实现各种不同的航线。我们可以把航线的一端截去,再拼接一段新的航线,我们可以有任何细节的嵌套,而且可以充分利用一切可能有用的选项。

当然,我们现在还不需要这些选择。当不需要这些航线分段和不同的“门航段”时,不使用COMPOSITE模式也能很好地工作。设计模式应该仅仅在需要的时候才使用。

第12章 将设计模式应用于模型 - 图10 图12-8 使用COMPOSITE之后的类图

第12章 将设计模式应用于模型 - 图11 图12-9 表示了一个完整Route的实例

12.3 为什么没有介绍FLYWEIGHT

由于第5章中提到过FLYWEIGHT模式,因此你可能认为它是一种适用于领域模型的模式。事实上,FLYWEIGHT虽然是设计模式的一个典型的例子,却并不适用于领域模型。

当一个VALUE OBJECT集合(其中的值对象数目有限)被多次使用的时候(如房屋规划中电源插座的例子),那么把它们实现为FLYWEIGHT可能是有意义的。这是一个适用于VALUE OBJECT(但不适用于ENTITY)的实现选择。COMPOSITE模式与它的不同之处在于,组合模式的概念对象是由其他概念对象组成的。这使得组合模式既适用于模型,也适用于实现,这是领域模式的一个基本特征。

我并不打算把那些可以当作领域模式使用的设计模式完整地列出来。虽然我想不出一个把“解释器”(interpreter)用作领域模式的例子,但我也不能断言解释器不适用于任何一种领域概念。把设计模式用作领域模式的唯一要求是这些模式能够描述关于概念领域的一些事情,而不仅仅是作为解决技术问题的技术解决方案。