第11章 应用分析模式

深层模型和柔性设计并非唾手可得。要想取得进展,必须学习大量领域知识并进行充分的讨论,还需要经历大量的尝试和失败。但有时我们也能从中获得一些优势。一位经验丰富的开发人员在研究领域问题时,如果发现了他所熟悉的某种职责或某个关系网,他会想起以前这个问题是如何解决的。以前尝试过哪些模型?哪些是有效的?在实现中有哪些难题?它们是如何解决的?先前经历过的尝试和失败会突然间与新的情况联系起来。这些模式当中有一些已经记载到文献中供大家分享,这样我们就可以借鉴这些积累的经验。

与第二部分提出的基本构造块模式和第10章介绍的柔性设计原则相比,这些模式属于更高级和专用的模式,其中还涉及使用少量对象来表示某种概念。利用这些模式,可以避免一些代价高昂的尝试和失败过程,而直接从一个已经具有良好表达力和易实现的模型开始工作,并解决了一些可能难于学习的微妙的问题。我们可以从这样一个起点来重构和试验。然而,它们并不是现成的解决方案。

在《分析模式》一书中,Martin Fowler这样定义分析模式[Fowler 1997,p.8]:

分析模式是一种概念集合,用来表示业务建模中的常见结构。它可能只与一个领域有关,也可能跨越多个领域。

Fowler所提出的分析模式来自于实践经验,因此只要用在合适的情形下,它们会非常实用。对于那些面对着具有挑战性领域的人们,这些模式为他们的迭代开发过程提供了一个非常有价值的起点。“分析模式”这个名字本身就强调了其概念本质。分析模式并不是技术解决方案,他们只是些参考,用来指导人们设计特定领域中的模型。

但从这个名字中我们看不出分析模式也讨论了大量实现问题,包括一些代码。Fowler知道,在不考虑实际设计的情况下进行单纯的分析是有缺陷的。下面举一个很有趣的例子,在这个例子中,Fowler用更长远的眼光审视了模型选择的意义——考虑在部署之后,模型选择对系统长期维护的影响[Fowler 1997,p.151]。

当构建一个新的[会计]实务时,我们会创建一个新的过账规则(posting rule)的实例网。我们可以在完全不需要重新编译或构建系统的情况下实现它,因而不影响系统的运行。有时我们将不可避免地需要过账规则的某个新的子类型,但这种情况并不多见。

在一个成熟的项目上,模型选择往往是根据实用经验做出的。人们已经尝试了各种组件的多种实现方法。其中的一些实现已经被采用,有些甚至已经到了维护阶段。这些经验可以帮助人们避免很多问题。分析模式的最大作用是借鉴其他项目的经验,把那些项目中有关设计方向和实现结果的广泛讨论与当前模型的理解结合起来。脱离具体的上下文来讨论模型思想不但难以落地,而且还会造成分析与设计严重脱节的风险,而这一点正是MODEL-DRIVEN DESIGN坚决反对的。

用实际的例子比用单纯的抽象描述能够更好地解释分析模式的原则和应用。本章将给出两个示例,在这两个例子中,开发人员借鉴了[Fowler 1997]一书中提供的一个具有代表性的小范例(来自“Inventory and Accounting”一章)。本章只是为了讲解这两个例子而概述分析模式。显然,本章的目的不是对这种模式进行归纳分类,甚至对示例所使用的模式也没有做全面的解释。本章的重点是说明如何将它们集成到领域驱动的设计过程中。

示例 账户的利息计算

第10章展示了开发人员为某种专用会计应用程序去寻找更深层模型的各种可能途径。本示例是另外一个场景,这里开发人员将深入挖掘Fowler的《分析模式》一书,从中寻找有用的思想。

来复习一下。用于跟踪贷款和其他有息资产的应用程序将计算所产生的利息和手续费,并跟踪借方的付款情况。夜间会有一个批处理操作提取这些数字,并传递给原来的会计系统,并标明每个账目应该过账到哪个分类账中。这种设计虽然能工作,但使用起来却很麻烦,修改起来也很复杂,而且不易于交流沟通。

第11章 应用分析模式 - 图1 图11-1 初始的类图

开发人员决定读一下《分析模式》的第6章“Inventory and Accounting”。下面摘录了一些与之最为相关的内容。

《分析模式》中的账户模型

所有种类的业务应用程序都需要对账户进行跟踪,因为账户中保存了与数值有关的信息(通常是钱)。在很多应用程序中,仅跟踪账户总额是不够的,记录和控制账户总额的每次修改也很重要。这也是会计模型最基本的动机。

第11章 应用分析模式 - 图2 图11-2 一个基本的账户模型

通过插入一个Entry(项)可以向账户中增加数值,而插入一个负的Entry则可以从账户中减少数值。Entry永远不会被删除,因此整个历史就被保留下来。余额就是把所有Entry汇总得到的结果。这个余额可以实时计算,也可以被缓存,这是由Account接口封装的一个实现决策。

会计的一条基本原则就是账目的平恒。钱即不会无中生有,也不会凭空消失。它只能从一个账户转移到另一个账户。

第11章 应用分析模式 - 图3 图11-3 一个交易模型

这就是众所周知的“复式记账”(double-entry book-keeping)概念。每个贷方都有与之相应的借方。当然,像其他守恒定律一样,它只适用于封闭的系统,这个系统包含了入账和出账的所有明细。但很多简单的应用程序并不需要这么严格。

Fowler在他的书中介绍了这些模型的较全面的形式,并对各种折中选择做了大量讨论。

开发人员(开发人员1)通过阅读这些内容获得了一些新的思路。她把这章内容介绍给她的同事(开发人员2)看,这位同事正在与她一起编写利息计算逻辑,而且他还编写了夜间批处理程序。他们一起对模型作了粗略的修改——在模型中加进了一些在阅读该章时看到的模型元素。

第11章 应用分析模式 - 图4 图11-4 新提出的模型

然后他们找来领域专家(以下称专家)一起讨论新模型的思路。

开发人员1:利用这个新模型,我们可以在Interest Account中为每笔利息收入增加一个Entry,而不是只调整interestDueAmount。然后,另一个付款的Entry会使其平账。

专家:这样是不是就可以看到所有的应计(accrual)利息和付款历史了?这正是需要的功能。

开发人员2:我不确定这里使用“Transaction”(交易)是否完全正确。定义讲的是把钱从一个Account转移到另一个Account,而不是两个Entry在同一个Account中互相平衡。

开发人员1:这个问题很好,我也有些担心,因为书上似乎强调交易是瞬间建立的,而利息的付款可以过几天再进行。

专家:那些付款不一定要推迟几天,在支付时间上可以灵活处理。

开发人员1:那么这种担心就没有必要了。我想我们或许已经发现了一些隐含的概念。让Interest Calculator来创建Entry对象似乎确实更易理解。而且Transaction似乎把计算出的利息和付款巧妙地联系在一起了。

专家:为什么要把应计项目和付款联系在一起呢?它们在会计系统中是分开过账的。Account的平账才是主要的。沿着一个一个的Entry,我们就可以查出所有的账目。

开发人员2:你的意思是说不用跟踪利息是否已经支付这一点吗?

专家:当然需要跟踪。但它并不是你们所说的“一次应计项目/一笔付款”这种简单的模式。

开发人员2:实际上,如果不用考虑那种关联,很多事情都可以简化。

开发人员1:好的,这样如何?[拿来旧类图的复印件开始把修改的地方画出来]。顺便问一下,你好几次提到“应计项目”这个词,能确切地讲一下它的意思吗?

专家:当然可以。应计项目(accrual)是指在一笔支出或收入发生的时候把它记录到账目中,而永远不管现金实际是何时过账的。因此,利息每天都会计算,但只有在(举例来说)月末才会支付。

开发人员1:是的,我们确实需要这个词。好,现在这个图怎么样?

第11章 应用分析模式 - 图5 图11-5 还是原来的类图,只是把应计项目和付款分开了

开发人员1:现在,我们可以删掉与付款有关的所有复杂计算了,而且我们引入了“accrual”这个术语,它更好地表明了我们的意图。

专家:那么我们就不会有Account对象了吧?我本来还希望能够把应计项目、付款和余额等项都放到这个对象中呢。

开发人员1:是吗?!如果是那样的话,或许这么画就可以[拿起另一张图开始画起来]。

第11章 应用分析模式 - 图6 图11-6 基于账户的图,里面没有Transaction

专家:这看起来确实好极了!

开发人员2:批处理脚本也只需要简单的修改就能使用这些新对象。

开发人员1:新的Interest Calculator过几天才能使用。有好些个测试需要修改。但修改之后测试会更清楚。

两位开发人员开始基于新模型进行重构。他们在着手编写代码并加强设计时,又有了一些对模型进行精化的新想法。

通过更仔细的研究,他们决定为Entry创建两个子类——Payment和Accrual,因为他们发现这两个子类在应用程序中的职责稍有不同,而且都是非常重要的领域概念。但另一方面,无论Entry是因为手续费而产生的,还是因为利息产生的,其在概念和行为上都没有任何区别。它们只是出现在适当的Account中。

但遗憾的是,开发人员们发现,出于实现方面的考虑,他们不得不放弃最后这一次抽象。数据存储在关系表中,而且项目标准要求在不运行程序的情况下也能解释清楚这些表。这意味着要把手续费项和利息项分开保存到不同的表中。根据他们所使用的对象—关系映射框架,将手续费项和利息项保存到不同表中的唯一方法就是创建具体的子类(Fee Payment、Interest Payment等)。如果换成别的基础设施,他们或许可以避免使用这些笨拙的子类。

我在这个大部分是虚构的故事中讲述这段小插曲的原因是想说明我们在现实中总是会遇到这类小的障碍。我们必须做出一些适当的折中选择然后继续前进,而不能因为这些小问题而改变MODEL-DRIVEN DESIGN的方向。

第11章 应用分析模式 - 图7 图11-7 实现之后的类图

新的设计更易于分析和测试,因为最复杂的功能已被封装到了SIDE-EFFECT-FREE FUNCTION中。剩下的命令代码很简单(因为它只需调用各种FUNCTION),并使用了ASSERTION。

有时,我们甚至想象不到,程序的一些部分也能从领域模型获益。它们可能一开始很简单,并一步步机械地演变。它们看上去就像是复杂的应用程序代码,而不是领域逻辑。分析模式在找到这些盲点方面特别有用。

在下一个例子中,一位开发人员对夜间批处理程序的内部机制产生了新的想法,以前他并没有从领域的角度来考虑这一问题。

示例 对夜间批处理程序的深入理解

几星期后,改进后的基于Account的模型基本完成了。如时常发生的那样,当新设计更加清晰之后,它就暴露出其他一些问题。开发人员(开发人员2)在修改夜间批处理程序以使之与新设计交互的时候,发现批处理程序的行为与《分析模式》一书中所讲的一些概念有联系。下面就是他发现的一些最相关的概念:

过账规则

会计系统经常提供同一个基本财务信息的多种视图。一个账户可能用于跟踪收入,而另一个账户可能用于跟踪该收入的估税。如果我们希望系统自动更新估税总额,那么这两个账户的实现将会彼此紧密关联。在有些系统中,大部分账目都是由这些规则产生的,在这样的系统中,依赖逻辑会变得一团糟。即使是在规模不大的系统中,这样的交叉过账也会很复杂。减少这种缠杂不清的依赖的第一步是通过引入一个新对象来使这些规则明朗化。

第11章 应用分析模式 - 图8 图11-8 基本过账规则的类图

当过账规则的input账目收到一个新的Entry时,这个Entry就会触发过账规则。然后过账规则会生成一个新的Entry(基于它自己的计算方法),并将这个Entry插入它的“output”账目中。在工资系统中,工资Account中的Entry可能会触发一个过账规则,此规则计算30%的估计收入所得税,并将其作为一个Entry插入扣税Account中。

执行过账规则

过账规则建立了各个Account之间概念上的依赖性,但如果对这个模式的使用仅限于此,那么它仍然很难使用。在依赖性设计中,最复杂的部分是更新的时机和控制措施。Fowler讨论了3种选择:

(1)“主动触发”(Eager firing)是最直接的方式,但通常也最不实用。每当一个Entry被插入到Account中时,它立即就触发过账规则,并立即进行所有更新。

(2)“基于Account的触发”允许推迟处理。在过后的某个时刻,向Account发送一条消息,令其触发过账规则,来处理自从上一次触发以来所插入的所有Entry。

(3) 最后,“基于过账规则的触发”由外部代理来启动,它通知过账规则触发。过账规则负责查找自从上次触发以来插入到其输入Account中的所有Entry。

尽管在一个系统中可以混合使用各种触发模式,但每组特定的规则都需要有一个明确定义的“启动点”(应该在何时启动),还要定义由谁负责查找插入到输入的Account中的Entry。将这三种触发模式添加到UBIQUITOUS LANGUAGE中对于成功使用这种模式具有至关重要的意义,这与模型对象定义本身同等重要。这样,触发的概念就不再模糊了,而且还能直接指导决策,从而获得一组明确定义的可选方案。这些触发模式揭示出了一个很容易被忽略的重点,并且丰富了我们的词汇,从而使讨论更清晰。

开发人员2需要找个人来讨论他的新思路。他找到了同事(开发人员1),开发人员1原来主要负责建立应计项目(accrual)的模型。

开发人员2:有的时候,夜间批处理程序成为一个隐藏问题的地方。脚本的行为中隐含了领域逻辑,而且正在变得越来越复杂。很长时间以来,我一直想用MODEL-DRIVEN DESIGN的方法来修改一下批处理,将领域层分离出来,并使脚本本身成为领域层之上的一个简单的层。但我一直没有想出这个领域模型应该是什么样的。看上去它似乎只是一些操作步骤,而把它们实现为对象没什么实际意义。但当我读完《分析模式》一书中有关Posting Rule的内容后,获得了一些思路。这个图就是我所想到的[递过来一张草图]。

第11章 应用分析模式 - 图9 图11-9 在批处理中使用过账规则的一个思路

开发人员1:Posting Service是指什么?

开发人员2:这是个FACADE,它提供了会计应用程序的API,并且将其呈现为一个SERVICE。实际上我使用它已经有一段时间了,主要用来简化批处理代码,而且它也为我提供了一个INTENTION-REVEALING INTERFACE,可用于向老系统过账。

开发人员1:很有趣,那么你打算为这些Posting Rule(过账规则)使用哪种触发模式?

开发人员2:我还没有想那么多。

开发人员1:“主动触发”可能适用于Accrual,因为批处理程序实际上通知Asset插入Accrual,但“主动触发”可能不适用于Payment,因为Payment是在白天输入的。

开发人员2:不管怎样,我认为我们都不希望把计算方法与批处理程序特别紧密地联系到一起。如果我们决定在一个不同的时间来触发利息计算,那么情况将会是一团糟。而且从概念上看,这也是不正确的。

开发人员1:这听上去有点像“基于Posting Rule的触发”。由批处理程序通知每个Posting Rule去执行,然后规则找出相应的新Entry,并完成其工作。这种思路基本上就与你画的图中表现出来的思路差不多吧。

开发人员2:这样在批处理设计中就不会产生很多依赖,而且它也易于控制了,看样子不错。

开发人员1:我没有完全明白这些对象是如何与Account和Entry交互的。

开发人员2:我也没完全明白。那本书中的示例在Account和Posting Rule之间建立了直接联系。在某种程度上这是合乎逻辑的,但我认为它并不完全适用于我们的情况。我们每次都需要用数据来实例化这些对象,因此要使用这种方法,必须知道应用哪条规则。同时,Asset对象知道每个Account的内容,因此也知道应用哪条规则。不管怎么说,这个模型的其他方面呢?

开发人员1:虽然我讨厌过分挑剔,但我确实认为Method的使用不正确。我认为在概念上Method是用于计算要过账的总额的,比方说,在收入上计算20%的扣税。但我们的情形很简单:它始终是全额过账。我想Posting Rule本身应该是知道要过账给哪个Account的,这个Account对应于我们的ledger name(分类账名称)。

开发人员2:哦,那么如果让Posting Rule负责查知正确的ledger name,我们可能就完全不需要Method了。

实际上,选择正确的ledger name这件事情变得越来越复杂了。它已经是收入类型(手续费或利息)与“Asset类别”(公司对每种Asset所使用的分类)的组合了。我希望新模型能够在解决这个复杂性上有所帮助。

开发人员1:好的,我们就把重点集中在这里。Posting Rule负责根据Account的属性来选择Ledger。现在,我们可以先用一种简单直接的方式来处理资产类型以及利息与收入之间的区分。将来,我们会有一个OBJECT MODEL,可以通过改进这个模型来处理更复杂的情形。

开发人员2:在这方面我还要多考虑一下。我会再仔细研究一番,再把模式读一遍,然后再来尝试解决这个问题。明天下午我能再次和你讨论这个问题吗?

在接下来的几天时间里,这两位开发人员设计出了一个模型,并对代码进行了重构,使得批处理程序只是简单地依次访问各个Asset,并向每个Asset发送几条非常浅显易懂的消息,然后提交数据库事务。复杂性被转移到领域层中,领域层中的对象模型使问题变得更加明确,也更抽象。

第11章 应用分析模式 - 图10 图11-10 含有过账规则的类图

第11章 应用分析模式 - 图11 图11-11 显示了规则触发的序列图

在一些细节问题上,这两位开发人员开发的模型与《分析模式》中给出的相差甚远,但他们认为二者在概念本质上还是相同的。有一个问题令他们稍感不安,那就是在Posting Rule的选择中把Asset牵扯进来了。之所以这样做,是因为Asset知道每个Account的性质(手续费或利息),而且它也是脚本的自然访问点。如果让规则对象直接与Account 发生关联,这些对象在每次实例化时(每次运行批处理程序时)都需要与Asset对象进行协作。可他们没有这样做,而是让Asset对象通过SINGLETON访问来查找这两个相关规则,并把对应的Account传递给它们。这样一来代码就变得更直接了,因此这是一个正确的决定。

从概念上看,他们都感到更好的做法是让Posting Rule只与Account发生关联,而令Asset只负责生成Accrual。他们希望等到有了后续的重构和更深入的理解之后再回头看这个问题,并找到一种将职责分离得更清楚而又不影响代码明确性的方法。

分析模式是很有价值的知识

当你可以幸运地使用一种分析模式时,它一般并不会直接满足你的需求。但它为你的研究提供了有价值的线索,而且提供了明确抽象的词汇。它还可以指导我们的实现,从而省去很多麻烦。

我们应该把所有分析模式的知识融入到知识消化和重构的过程中,从而形成更深刻的理解,并促进开发。当我们应用一种分析模式时,所得到的结果通常与该模式的文献中记载的形式非常相像,只是因具体情况不同而略有差异。但有时完全看不出这个结果与分析模式本身有关,然而这个结果仍然是受该模式思想的启发而得到的。

但有一个误区是应该避免的。当使用众所周知的分析模式中的术语时,一定要注意,不管其表面形式的变化有多大,都不要改变它所表示的基本概念。这样做有两个原因,一是模式中蕴含的基本概念将帮助我们避免问题,二是(也是更重要的原因)使用被广泛理解或至少是被明确解释的术语可以增强UBIQUITOUS LANGUAGE。如果在模型的自然演变过程中模型的定义也发生改变,那么就要修改模型名称了。

很多对象模型都有文献资料可查,其中有些对象模型专门用于某个行业中的某种应用,而有些则是通用模型。大部分对象模型都有助于开阔思路,但只有为数不多的一些模型精辟地阐述了选择这些模式的原理和使用的结果,而这些才是分析模式的精华所在。这些精化后的分析模式大部分都很有价值,有了它们,可以免去一次次的重复开发工作。尽管我们不大可能归纳出一个包罗万象的分析模式类目,但针对具体行业的类目还是能够开发出来的。而且在一些跨越多个应用的领域中适用的模式可以被广泛共享。

这种对已组织好的知识的重复利用完全不同于通过框架或组件进行的代码重用,但是二者唯一的共同点是它们都提供了一种新思路的萌芽,而这种新思路先前可能并不十分明晰。一个模型,甚至一个通用框架,都是一个完整的整体,而分析则相当于一个工具包,它被应用于模型的一些部分。分析模式专注于一些最关键和最艰难的决策,并阐明了各种替代和选择方案。它们提前预测了一些后期结果,而如果单靠我们自己去发现这些结果,可能会付出高昂的代价。