五、应用分析模式
深层模型和柔性设计并非唾手可得。想要取得进展,必须学习大量领域知识并进行充分的讨论,还需要经历大量的尝试和失败。在实际的研究领域问题实践时,有一些成熟的模式可以供我们借鉴和套用。这样我们可以从这个起点来重构和试验,虽然它们不是现成的解决方案。
在《分析模式》一书中,Martin Fowler这样定义分析模式:
分析模式是一种概念集合,用来表示业务建模中的常见结构。它可能只与一个领域有关,也可能跨越多个领域。
Fowler提出的分析模式来自于实践经验,对于那些面对具有挑战性领域的人们,这些模式为他们的迭代开发过程提供了一个非常有价值的起点。
在一个成熟的项目上,模型选择往往是根据实用经验做出的。这些经验可以帮助人们避免很多问题,分析模式的最大作用是借鉴其他项目的经验,把那些项目中有关设计方向和实现结果的广泛讨论与当前模型的理解结合起来。脱离具体的上下文来讨论模型思想不但难以落地,而且还会造成分析与设计严重脱节的风险,而这一点正是Model-Driven Design坚决反对的。
分析模式是很有价值的知识
当你可以幸运地使用一种分析模式时,它一般不会直接满足你的需求。但它为你的研究提供了有价值的线索,而且提供了明确抽象的词汇。它还可以指导我们的实现,从而省去很多的麻烦。
我们应该把所有分析模式的知识融入知识消化和重构的过程中,从而形成更深刻的理解,并促进开发。当我们应用一种分析模式时,所得到的结果通常与该模式的文献中记载的形式非常相像,只是因具体情况不同而略有差异。但有时完全看不出这个结果与分析模式本身有关,然而这个结果仍然是受该模式思想的启发而得到的。
但有一个误区是应该避免的。当使用众所周知的分析模式中的术语时,一定要注意,不管其表面形式的变化有多大,都不要改变它所表示的基本概念。这样做有两个原因,一是模式中蕴含的基本概念将帮助我们避免问题,二是(也是更重要的原因)使用被广泛理解或至少是被明确解释的术语可以增强Ubiquitous Language。如果在模型的自然演变过程中模型的定义也发生改变,那么就要修改模型名称了。
很多对象模型都有文献资料可查,其中有些对象模型专门用于某个行业中的某种应用,而有些则是通用模型。大部分对象模型都有助于开阔思路,但只有为数不多的一些模型精辟地阐述了选择这些模型的原理和使用的结果,而这些才是分析模式的精华所在。这些精华化后的分析模式大部分都很有价值,有了它们,可以免去一次次的重复开发工作。尽管我们不在可能归纳聘个包罗万象的分析模式类目,但针对具体行业的类目还是能够开发出来的。而且在一些跨越多个应用的领域中适用的模式可以被广泛共享。
这种对已组织好的知识的重复利用完全不同于通过框架或组件进行的代码重用,但是二者唯一的共同点是它们都提供了一种新思路的萌芽,而这种新思路先前可能并不十分明晰。一个模型,甚至一个通用框架,都是一个完整的整体,而分析则相当于一个工具包,它被应用于模型的一些部分。分析模式专注于一些最关键和最艰难的决策,并阐明了各种替代和选择方案。它们提前预测了一些后期结果,而如果单靠我们自己去发现这些结果,可能会付出高昂的代价。
六、将设计模式应用于模型
设计模式与领域模式之间有什么区别?《设计模式》这部经典著作的作者为初学者指出了以下事实【Gamma et al. 1995,p.3】;
立场不同会影响人们如何看待什么是模式以及什么不是模式。一个人所认为的模式在另一个人看来可能是基本构造块。本书将在一定的抽象层次上讨论模式。设计模式并不是指像链表和散列表那样可以被封装到类中并供人们直接重用的设计,也不是用于整个应用程序或子系统的复杂的、领域特定的设计。本书中的设计模式是对一些交互的对象和类的描述,我们通过定制这些对象和类来解决特定上下文中的一般设计问题。
在《设计模式》中,有些模式可以用作领域模式,但这样做的时候,需要变换一下重点。除了《设计模式》中介绍的模式外,近年来还出现了很多技术设计模式。有些模式反映了在一些领域中出现的深层概念。这些模式都有很大的利用价值。为了在领域驱动设计中充分利用这些模式,我们必须同时从两个角度看待它们:从代码的角度来看他们是技术设计模式,从模型的角度来看它们是概念模式。
我们将把《设计模式》所介绍的特定模式作为样例,来说明如何将人们所认为的设计模式应用到领域模型中,而且这个例子还将澄清技术设计模式与领域模式之间的区别。
1、模式:Strategy(也称为Policy)
领域模型包含一些并非用于解决技术问题的过程,将它们包含进来是因为它们对处理问题领域具有实际的价值。当必须从多个过程中进行选择时,选择的复杂性再加上多个过程本身的复杂性会使局面失去控制。
当对进程建模时,我们经常会发现过程有不止一种合理的实现方式,而如果把所有的可选项都写到过程的定义中,定义就会变得得臃肿而复杂,而且可代我们选择的实际行为也会因为混杂在其他行为中而显得模糊不清。
我们希望把这些选择从过程的主体概念中分离出来,这样即能够看清主体概念,也能更清楚地看到这些选择。软件设计社区中众所周知的Strategy模式就是为了解决这个问题的,虽然它的侧重点在于技术方面。这里,我们把它当成模型中的一个概念来使用,并在该模型的代码实现中把它反映出来。我们同样也需要把过程中极易发生变化的部分与那些更稳定的部分分离开。
因此:
我们需要把过程中的易变部分提取到模型的一个单独的"策略"对象中。将规则与它所控制的行为区分开。按照Strategy设计模式来实现规则或可替换的过程。策略对象的多个版本表示了完成过程的不同方式。
2、模式:Composite
在对复杂的领域进行建模时,我们经常会遇到由多个部分组成的重要对象,这些部分本身又由其他一些部分组成,依次类推,有时甚至会出现任意深度的嵌套。在一些领域中,各层嵌套在概念上是有区别的,但在另一些领域中,各个部分与它们所组成的整体是完全相同的事物,只是规模较小一些而已。
当嵌套容器的关联性没有在模型中反映出来时,公共行为必然会在层次结构的每一层重复出现,而且嵌套也变得僵化(例如,容器通常不能包含同一层中的其他容器,而且嵌套的层数也是固定的)。客户必须通过不同的接口来处理层次结构中的不同层,尽管这些层在概念上可能没有区别。通过层次结构来递归地收集信息也变得非常复杂。
当在领域中应用任何一种设计模式时,首先关注的问题应该是模式的意图是否确实适合领域概念。以递归的方式遍历一些相互关联对象确实比较方便,但它们是否真的存在整体-部分层次结构?你是否发现可以通过某种抽象方式把所有部分都归到同一概念中?如果你确实发现了这种抽象方式,那么使用Composite可以令模型的这些部分变得更清晰,同时使你能够借助设计模式所提供的那些经过深思熟虑的设计及实现的考量。
因此:
定义一个把Composite的所有成员都包含在内的抽象类型。在容器上实现那些查询信息的方法时,这些方法返回由容器内容所汇总的信息。而"叶"节点则基于它们自己的值来实现这些方法。客户只需使用抽象类型,而无需区分"叶"和容器。
相对而言,这是一种明显的结构层面上的模式,但设计人员通常不会主动地充实它的操作方面。Composite模式在每个结构层上都提供了相同的行为,而且无论是较小的部分还是较大的部分,都可以对这些部分提出一些有意义的问题,这些问题能够透明地反映出它们的构成情况。这种严格的对称是组合模式具有强大能力的关键所在。
3、为什么没有介绍FlyWeight
以前提到过FlyWeight模式,因此你可能认为它是一种适用于领域模型的模式。事实上,FlyWeight虽然是设计模式的一个典型例子,却并不适用于领域模型。
当一个ValueObject集合(其中的值对象数目有限)被多次使用的时候,那么它们实现FlyWeight可能是有意义的。这是一个适用于ValueObject(但不适用于Entity)的实现选择。Composite模式与它的不同之处在于,组合模式的概念对象是由其他概念对象组成的。这使用组合模式即适用于模型,也适用于实现,这是领域模式的一个基本特征。
把设计模式用作领域模式的唯一要求是这些模式能够描述关于概念领域的一些事情,而不仅仅是作为解决技术问题的技术解决方案。
七、通过重构得到更深层的理解
通过重构得到更深层的理解是一个涉及很多方面的过程,归纳下有三件事必须要关注:
(1)以领域为本
(2)用一种不同的方式来看待事件
(3)始终坚持与领域专家对话
一提到传统意义上的重构,我们头脑中就会出现这样一幅场景:一两位开发人员坐在键盘前面,发现一些代码可以改进,然后立即动手修改代码。这个过程应该一直进行下去,但它并不是重构过程的全部。
1、开始重构
与传统重构观点不同的昌,即使代码看上去很整洁的时候也可能需要重构,原因是模型的语言没有与领域专家保持一致,或者新需求不能被自然地添加到模型中。重构的原因也可能来自学习:当开发人员通过学习获得了更深刻的理解,从而发现了一个得到更清晰或更有用的模型的机会。
如何找到问题的病灶往往是最难和最不确定的部分。在这之后,开发人员就可以系统地找出新模型的元素。他们可以与同事和领域专家一起进行半脑风暴,也可以充分利用那些已经对知识做了系统性总结的分析模式或设计模式。
2、探索团队
不管问题的根源是什么,下一步都是要找到一种能够使模型表达变得更清楚和更自然的改进方案。这可能只需要做一些简单、明显的修改,只需要几个小时即要完成。在这种情况下,所做的修改类似于传统重构。但寻找新模型可能需要更多时间,而且需要更多人参与。
修改的发起者会挑选几们开发人员一起工作,这些开发人员应该擅长思考该类问题,了解领域,或者掌握深厚的建模技巧。如果涉及一些难以捉摸的问题,他们还要请一位领域专家加入。这个由4~5个人组成的小组会到会议室或咖啡厅进行头脑风暴,时间为半小时至一个半小时。在这个过程中,他们画一些UML蓝图,并试着用对象来走查场景。他们必须保证主题专家能够理解模型并认为模型有用。当发现了一些令他们满意的新思路后,他们就回去编码,或者决定再多考虑几天,先回去做点别的事情。几天之后,这个小组再次碰头,重复上面的过程。这时,他们已经对前几天的想法有了更深入的理解,因此更加自信了,并且得出了一些结论。他们回到计算机前,开始对新设计进得编码。
要想保证这个过程的效率,需要注意几个关键事项。
- 自主决定。
- 注意范围和休息。
- 练习使用Ubiquitous Language。
3、借鉴先前的经验
有时我们可以从分析模式中汲取他人的经验。这些经验对于帮助我们读懂领域起到了一定的作用,但分析模式是专门针对软件开发的,因此应该直接根据我们自己在领域中实现软件的经验来利用这些模式。分析模式可以提供精细的模型概念,并帮助我们避免很多错误。但它们并不是现成的"菜谱"。它们只是为知识消化过程提供了一些供给。
随着零散知识的归纳,必须同时处理模型关注点和设计关注点。同样,这并不意味着总是需要从头开发一切。当设计模式即符合实现需求,又符合模型概念时,通常就可以在领域层中应用这些模式。
同样,当一种常用的形式体系与领域的某个部分非常符合时,可以把这个部分提取出来,并根据它来修改形式系统的规则。这可以产生非常简练且易于理解的模型。
4、针对开发人员的设计
软件不仅仅是为用户提供的,也是为开发人员提供的。开发人员必须把他们编写的代码与系统的其他部分集成到一起。在迭代过程中,开发人员反复修改代码。开发人员应该通过重构得到更深层的理解,这样即能够实现柔性设计,也能够从这样一个设计中获益。
柔性设计能够清楚地表明它的意图。这样的设计使人们很容易看出代码的运行效果,因此也很容易预计修改代码的结果。柔性设计主要通过减少依赖性和副作用来减轻人们的思考负担。这样的设计是以深层次的领域模型为基础的,在模型中,只有那些对用户最重要的部分才具有较细的粒度。在这样的模型中,那些经常需要修改的地方能够各持很高的灵活性,而其他地方则相对比较简单。
5、重构的时机
如果一直等到完全证明了修改的合理性之后才去修改,那么可能要等待太长时间了。项目正承受巨大的耗支,推迟修改将使修改变得更难执行,因为要修改的代码已经变得更加复杂,并更深地嵌入到其他代码中。
持续重构渐渐被认为是一种"最佳实践",但大部分项目团队仍然对它抱有很大的戒心。人们虽然看到了修改代码会有风险,还要花费开发时间,但却不容易看到维持一个拙劣设计也有风险,而且迁就这种设计也要付出代价。想要重构的开发人员往往被要求证明其重构的合理性。虽然这看似合理,但这使得一个本来就很难进行的工作几乎变得不可能完成,而且会限制重构的进行。软件开发并不是一个可以完全预料到后果的过程,人们无法准确地计算出某个修改会带来哪些好处,或者是不做某个修改会付出多大代价。
在探索领域的过程中,在培训开发人员的过程中,以及在开发人员与领域专家进行思想交流的过程中,必须始终坚持把"通过重构得到更深层理解"作为这些工作的一部分。因此,当发生以下情况时,就应该进行重构了:
- 设计没有表达出团队对领域的最新理解
- 重要的概念被隐藏在设计中(而且你已经发现了把它们显现出来的方法)
- 发现了一个能令某个重要的设计部分变得更灵活的机会
我们虽然应该有这样一种积极的态度,但并不意味着可以随随便便做任何修改。在发布的前一天,就不要进行重构了。不要引入一些只顾炫耀技术能力而没有解决领域核心问题的"柔性设计"。无论一个"更深层的模型"看起来有多好,如果你不能说服领域专家们去使用它,那么就不要引入它。万事都不是痴心绝对的,但如果某个重构对我们有利,那么不妨在这个方向上大胆前进。
6、危机就是机遇
在达尔文创立进化论后的一个多世纪中,人们一直认为标准的进化模型就是物种随着时间缓慢地改变。突然之间,这个模型在20世纪70年代被"间断平衡"模型取代了。它对原有进化论进行了扩展,认为长期的缓慢变化或稳定变化会被相对来说很短的、爆发性的快速变化所打断。然后事物会进入一个新的平衡。软件开发与物种进化之间的不同点是前者具有明确的方向(虽然在某些项目上可能并不明显),尽管如此软件开发仍遵循这种进化规律。
传统意义上的重构听起来是一个非常稳定的过程。但通过重构得到更深层理解往往不是这样的。在对模型进行一段时间稳定的改进后,你可能突然有所顿悟,而这会改变模型中的一切。这些突破不会每天都发生,然而很大一部分深层模型和柔性设计都来自这些突破。
这样的情况往往看起来不像是机遇,而更像是危机。例如,你突然发现模型中有一些明显的缺陷,在表达方面显示出一个很大的漏洞,或存在一些没有表达清楚的关键区域。或者有些描述是完全错误的。
这些都表明团队对模型的理解已经达到了一个新的水平。他们现在站在更高的层次上发现了原有模型的弱点。他们可以从这种角度构思一个更好的模型。
通过重构得到更深层理解是一个持续不断的过程。人们发现一些隐含的概念,并把它们明确地表示出来。有些设计部分变得更具有柔性,或许还采用了声明式的风格。开发工作一下子到了突破的边缘,然后开发人员跨越这条界线,得到了一个更深层次的模型,接下来又重新开始了稳步的改进过程。
八、参考文档
DOMAIN-DRIVERN DESIGN
TACKLING COMPLEXITY IN THE HEART OF SOFTWARE
领域驱动设计
软件核心复杂性应对之道
【美】Eric Evans 著 赵俐 盛海艳 刘霞 等 译