【 第三部分 】通过重构来加深理解
第8章:突破
一般来说,持续重构让事物逐步变得有序。代码和模型的每一次精化都让开发人员有了更加清晰的认识。这使得理解上的突破成为可能。之后,一系列快速的改变得到了更符合用户需要并更加切合实际的模型。其功能性及说明性急速增强,而复杂性却随之消失。这种突破不是某种技巧,而是一个事件。它的困难之处在于你需要判断发生了什么,然后再决定如何处理。
当突破带来更深层次的模型时,通常会令人感到不安。与大部分重构相比,这种变化的回报更多,风险也更高。而且突破出现的时机可能很不合时宜。尽管我们希望进展顺利,但往往事与愿违。过渡到真正的深层次模型需要从根本上调整思路,并且对设计做大幅修改。在很多项目中,建模和设计工作最重要的进展都来自于突破。
不要试图去制造突破,那只会使项目陷入困境。通常,只有在实现了许多适度的重构后才有可能出现突破。在大部分时间里,我们都在进行微小的改进,而在这种连续的改进中模型深层含义也会逐渐显现。
要为突破做好准备,应专注于知识消化过程,同时也要逐渐建立健壮的UBIQUITOUS LANGUAGE。寻找那些重要的领域概念,并在模型中清晰地表达出来。精化模型,使其更具有柔性。提炼模型。利用这些更容易掌握的手段使模型变得更清晰,这通常会带来突破。
不要犹豫着不去做小的改进,这些改进即使脱离不开常规的概念框架,也可以逐渐加深我们对模型的理解。不要因为好高骛远而使项目陷入困境。只要随时注意可能出现的机会就够了。
【学习心得】:我自己手头上目前持有一个运行了近十年的千万级用户系统,至今还在持续运营和追加功能。对于以上那些突破的感受,我太有体会了。整个项目总共经历了两次大的突破,以及无数次小突破。而每一次突破都十分痛苦,但快乐着。离不开团队的坚持,离不开团队的持续学习,更离不开团队吃苦精神。
第9章:将隐式概念转变为显示概念
深层建模听起来很不错,但是我们要如何时间它呢?深层模型之所以强大是因为它包含了领域的核心概念和抽象,能够以简单灵活的方式表达出基本的用户活动、问题以及解决方案。
若开发人员识别出设计中隐含的某个概念或者在讨论中收到启发而发现一个概念时,就会对领域建模和响应的代码进行许多转换,在模型中加入一个或多个对象或关系,从而将此概念显示地表达出来。有时,这种从隐式概念到显示概念的转换可能就是一次突破。
概念挖掘
1、倾听领域专家使用的语言。
有没有一些术语能够简洁地表达出复杂的概念?他们有没有纠正过你的用词(也许是很委婉的提醒)?当你使用某个特定词语时,他们脸上是否已经不再流露出迷惑的表情?这些都是暗示了某个概念也许可以改进模型。
2、检查不足之处。
你所需要的概念并不总是浮在表面上,也绝不仅仅是通过对话和文档就能让它显现出来。有些概念可能需要你自己去挖掘和创造。要挖掘的地方就是设计中最不足的地方,也就是操作复杂且难于理解的地方。每当有新需求时,似乎都会让这个地方变得更加复杂。有时,你很难意识到模型中丢失了什么概念。也许你的对象能够实现所有的功能,但是有些职责的实现却很笨拙。而有时,你虽然能够意识到模型中丢失了某些东西,但是却无法找到解决方案。
3、思考矛盾之处。
由于经验和需求的不同,不同的领域专家对同样的事情会有不同的看法。即使是同一个人提供的信息,仔细分析后也会发现逻辑上不一致的地方。在挖掘程序需求的时候,我们会不断遇到这种令人烦恼的矛盾,但它们也为深层模型的实现提供了重要线索。有时矛盾只是术语说法上的不一致,有些则是由于误解而产生的。但还有一种情况是专家们会给出相互矛盾的两种说法。
4、查阅书籍。
在寻找模型概念时,不要忽略一些显而易见的资源。在很多领域中,你都可以找到解释基本概念和传统思想的书籍。你依然需要与领域专家合作,提炼与你的问题相关的那部分知识,然后将其转化为适用于面向对象软件的概念。但是,查阅书籍也许能够使你一开始就形成一致且深层的认识。
5、尝试,再尝试。
并不是所有这些方向性的改变都毫无用处。每次改变都会把开发人员更深刻的理解添加到模型中。每次重构都使设计变得更灵活且为那些可能需要修改的地方做好准备。我们其实别无选择。只有不断尝试才能了解什么有效什么无效。企图避免设计上的失误将会导致开发出来的产品质量劣质,因为没有更多的经验可用来借鉴,同时也会比进行一系列快速实验更加费时。
如何为那些不太明显的概念建模?
1、显示的约束。
约束是模型概念中非常重要的类别。它们通常是隐含的,将它们显式地表现出来可以极大地提高设计质量。
(例子)为显示表达超订策略而重构的模型
2、将过程建模为领域对象。
首先要说明的是,我们都不希望过程变成模型的主要部分。对象是用来封装过程的,这样我们只需要考虑对象的业务目的或意图就可以了。就像我们以上用来安排货运路线的运输系统例子,安排路线的过程具有业务意义。SERVICE是显示表达这种过程的一种方式,同时它还会降异常复杂的算法封装起来。
如果过程的执行有多种方式,那么我们也可以用另一种方法来处理它,那就是将算法本身或其中的关键部分放到一个单独的对象中。这样,选择不同的过程就变成了选择不同的对象,每个对象都表示一种不同的STRATEGY(策略)。
那么,过程是应该被显示表达出来,还是应该被隐藏起来呢?区分的方法很简单:它是经常被领域专家提起呢,还是仅仅被当作计算机程序机制的一部分?
约束和过程是两大类概念模型,当我们用面向对象语言编程时,不会立即想到它们,然而它们一旦被我们视为模型元素,就真的可以让我们的设计更为清晰。
3、模式:SPECIFICATION
业务规则通常不适合作为ENTITY和VALUE OBJECT的职责,而且规则的变化和组合也会被掩盖领域对象的基本含义。但是将规则移出领域层的结果会更糟糕,因为这样一来,领域代码就不再表达模型了。
逻辑编程提供了一个概念,即“谓词”这种可分离、可组合的规则对象,但是要把这种概念用对象完全实现是很麻烦的。同时,这种概念过于通用,在表达设计意图方面,它的针对性不如专门的设计那么好。
因此:为特殊目的创建谓词形式的显式的VALUE OBJECT。SPECIFICATION就是一个谓词,可用来确定对象是否满足某些标准。
【学习心得】:项目的沟通成本之大,正是因为许多人的内心都会有一颗“自尊心”,我的领域我最懂,我的技术牛逼,你们业务人员可以一边站。所以,想成为一个合格的团队成员,至少得让自己能成为一个合格的聆听者。看似简单,但我在生活中就发现了自己并非一位很好的聆听者。错过了许多对自己有用的信息,更多还是用了许多自以为是的拍脑袋决策。
第10章:柔性设计
为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化将停滞不前,设计必须要让人们乐于使用,而且易于做出修改。这就是柔性设计(supple design)。
很多过度设计(overengineering)借着灵活性的名义而得到合理的外衣。但是,过多的抽象层和间接设计常常成为项目的绊脚石。看一下真正为用户带来强大功能的软件设计,你常常会发现一些简单的东西。简单并不容易做到。
模式:INTENTION-REVEALING INTERFACES(意图揭示接口)
如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。当某个人开发的对象或操作被别人使用时,如果使用这个组件的新的开发者不得不根据其实现来推测其用途,那么他推测出来的可能并不是那个操作或类的主要用途。如果这不是那个组件的用途,虽然代码暂时可以工作,但设计的概念基础已经被误用了,两位开发人员的意图也是背道而驰。
因此:在命名类和操作时要描述它们的效果和目的,而不是表露它们是通过何种方式达到目的的。这样可以使客户开发人员不必去理解内部细节。这些名称应该与UBIQUITOUS LANGUAGE保持一致,以便团队成员可以迅速推断出它们的意义。在创建一个行为之前先为它编写一个测试,这样可以促使你站在客户开发人员的角度上来思考它。所有复杂的机制都应该封装到抽象接口的后面,接口只表明意图,而不表明方式。
模式:SIDE-EFFECT-FREE FUNCTION(无副作用功能)
大多数操作都会调用其他的操作,而后者又会调用另外一些操作。一旦形成这种任意深度的嵌套,就很难预测调用一个操作将要产生的所有后果。第二层和第三层操作的影响可能并不是客户开发人员有意为之的,也是它们就变成了完全意义上的副作用(任何对未来操作产生影响的系统状态改变都可以成为副作用)。
多个规则的相互作用或计算的组合产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及它所调用的其他方法的实现。如果开发人员不得不“揭开接口的面纱”,那么接口的抽象作用就受到了限制。如果没有了可以安全地预见到结果的抽象,开发人员就必须限制“组合爆炸”,这就限制了系统行为的丰富性。
因此:尽可能把程序的逻辑放到函数(返回结果而不产生副作用的操作称为函数)中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令(引起明显的状态改变的方法)隔离到不返回领域信息的、非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到VALUE OBJECT中,这样可以进一步控制副作用。
模式:ASSERTION(断言)
把复杂的计算封装到SIDE-EFFECT-FREE FUNCTION中可以简化问题,但实体仍然会留有一些有副作用的命令,使用这些ENTITY的人必须了解使用这些命令的后果。
如果操作的副作用仅仅是由它们的实现隐式定义的,那么在一个具有大量相互调用关系的系统中,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径来跟踪程序的执行,封装完全失去了价值。跟踪具体的执行也使抽象失去了意义。
因此:把操作的后置条件和类及AGGREGATE的固定规则表达清楚。如果在你的编程语言中不能直接编写ASSERTION,那么就把它们编写成自动的单元测试。还可以把它们写到文档或图中(如果符合项目开发风格的话)。寻找在概念上内聚的模型,以便使开发人员更容易推断出预期的ASSERTION,从而加快学习过程并避免代码矛盾。
INTENTION-REVEALING INTERFACE清楚地表明了用途,SIDE-EFFECT-FREE FUNCTION和ASSERTION使我们能够更准确地预测结果,因此封装和抽象更加安全。
模式:CONCEPTUAL CONTOUR(概念轮廓)
如果把模型或设计的所有元素都放在一个整体的大结构中,那么它们的功能就会发生重复。外部接口无法给出客户可能关心的全部信息。由于不同的概念被混合在一起,它们的意义变得很难理解。
而另一方面,把类和方法分解开也可能是毫无意义的,这会使客户更复杂,迫使客户对象去理解各个细微部分是如何组合在一起的。更糟的是,有的概念可能会完全丢失。铀原子的一半并不是铀。而且,粒度的大小并不是唯一要考虑的问题,我们还要考虑粒度是在哪种场合下使用的。
因此:把设计元素(操作、接口、类和AGGREGATE)分解为内聚单元,在这个过程中,你对领域中一切重要划分的直观认识也要考虑在内。在连续的重构过程中观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层CONCEPTUAL CONTOUR。使模型与领域中那些一致的方面(正是这些方面使得领域成为一个有用的知识体系)相匹配。
我们的目标是得到一组可以在逻辑上组合起来的简单接口,使我们可以用UBIQUITOUS LANGUAGE进行合理的表述,并且使那些无关的选项不会分散我的注意力,也不增加维护负担。但这通常是通过重构才能得到的结果,很难在前期就实现。而且如果仅仅是从技术角度进行重构,可能永远也不会出现这种结果;只有通过重构得到更深层次的理解,才能实现这样的目标。
INTENTION-REVEALING INTERFACE使客户能够把对象表示为有意义的单元,而不仅仅是一些机制。SIDE-EFFECT-FREE FUNCTION和ASSERTION使我们可以安全地使用这些单元,并对它们进行复杂的组合。CONCEPTUAL CONTOUR的出现使模型的各个部分变得更加稳定,也使得这些单元更直观,更易于使用和组合。
模式:STANDALONE CLASS(独立的类)
即使是在MODULE内部,设计也会随着依赖关系的增加而变得越来越难以理解。这加重了我们的思考负担,从而限制了开发人员能处理的设计复杂度。隐式概念比显式引用增加的负担更大了。
低耦合是对象设计的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变得完全独立了,这就使得我们可以单独地研究和理解它。每个这样的独立类都极大地减轻了因理解MODULE而带来的负担。
尽力把最复杂的计算提取到STANDALONE CLASS(独立的类)中,实现此目的的一种方法是从存在大量依赖的类中将VALUE OBJECT建模出来。低耦合是减少概念过载的最基本方法。独立的类是低耦合的极致。
模式:CLOSURE OF OPERATION(闭合操作)
两个实数相乘,结果仍为实数(实数是所有有理数和所有无理数的集合)。由于这一点永远成立,因此我们说实数的“乘法运算是闭合的”:乘法运算的结果永远无法脱离实数这个集合。当我们对集合中的任意两个元素组合时,结果仍在这个集合中,这就叫做闭合操作。
——The Math Forum,Drexel University
加法运算是实数集中的闭合运算。数学家们都极力避免去引入无关的概念,而闭合运算的性质正好为他们提供了这样一种方式。
因此:在适当情况下,在定义操作时让它的返回类型与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引人对其他概念的任何依赖。
【学习心得】:经历了(还在经历)一个近十年的项目,我想自己还是比较有资格谈谈柔性设计的感受。没有学习这些柔性概念之前,我们能持续高效并运行开发一个项目那么长时间,功劳归于一个重要的原则:简单。起初,整个团队都缺乏以上这些实用的模式理论作为参考,但大家都有秉承着一个“简单”的共同原则,其实不知不觉中摸着石头过河,在无数次重构中逐渐跟以上模式契合起来。当然,如果我们能提前认识这些基础的理论基础知识,我想不必要的弯路会少走许多。也当然,系统还在不断完善中,现在认识也不晚。
第11章:应用分析模式
在《分析模式》一书中,Martin Fowler这样定义分析模式:
分析模式是一种概念集合,用来表示业务建模中的常见结构。它可能只与一个领域有关,也可能跨多个领域。
Fowler所提出的分析模式来自于实践经验,因此只要用在合适的情形下,它们会非常实用。对于那些面对着具有挑战性领域的人们,这些模型为他们的迭代开发过程提供了一个非常有价值的起点。“分析模式”这个名字本身就强调了其概念本质。分析模式不是技术方案,他们只是参考,用来指导人们设计特定领域中的模型。
分析模式最大的作用是借鉴其他项目的经验,把那些项目中有关设计方向和实现结构的广泛讨论与当前模型的理解结合起来。脱离具体的上下文来讨论模型思想不但难以落地,而且还会造成分析与设计严重脱节的风险,而这一点正是MODEL-DRIVEN DESIGN坚决反对的。
当你可以幸运地使用一种分析模式时,它一般并不会直接满足你的需求。但它为你的研究提供了有价值的线索,而且提供了明确抽象的词汇。它还可以知道我们的实现,从而省去很多麻烦。
我们应该把所有分析模式的知识融入知识消化和重构的过程中,从而形成更深刻的理解,并促进开发。当我们应用一种分析模式时,所得到的结果通常与该模式的文献中记载的形式非常想像,只是因具体情况不同而略有差异。但有时完全看不出这个结果与分析模式本身有关,然而这个结果仍然是受该模式思想的启发而得到的。
但有一个误区是应该避免的。当使用众所周知的分析模式中的术语时,一定要注意,不管其表面形式的变化有多大,都不要改变它所表示的基本概念。这样做有两个原因,一是模式中蕴含的基本概念将帮助我们避免问题,二是(也是更重要的原因)使用被广泛理解或至少是被明确理解的术语可以增强UBIQUITOUS LANGUAGE。如果在模型的自然演变过程中模型的定义也发生改变,那么就要修改模型名称了。
【学习心得】:本章节主要还是借助《分析模式》一书中的例子,用实践例子来分析系统是如何在演绎过程使用模型的。这种科学谨慎的做法,才是一个工程师的基本观念要求。
第12章:将设计模式应用于模型
在《设计模式》中,有些(但并非所有)模式可用作领域模式,但在这样使用的时候,需要变换一下重点。有些模式反映了一些在领域中出现的深层概念。这些模式都有很大的利用价值。为了在领域驱动设计中充分利用这些模式,我们必须同时从两个角度看待它们:从代码的角度来看它们是技术设计模式,从模型的角度来看它们就是概念模式。
模式:STRATEGY(也称POLICY)
策略模式:定义了一组算法,将每个算法封装起来,并使它们可以互换。STRATEGY允许算法独立于使用它的客户而变化[Gamma et al. 1995]
领域模型包含一些并非用于解决技术问题的过程,将它们包含进来是因为它们处理问题领域具有实际的价值。当必须从多个过程中进行选择时,选择的复杂性再加上多个过程本身的复杂性使局面失去控制。
因此:我们需要把过程中的易变部分提取到模型的一个单独的“策略”对象中。将规则与它所控制的行为区分开。按照STRATEGY设计模式来实现规则或可替换的过程。策略对象的多个版本表示了完成过程的不同方式。
模式:COMPOSITE
组合模式:将对象组织为树来表示部分—整体的层次结构。利用COMPOSITE,客户可以对单独的对象和对象组合进行同样的处理。[Gamma et al.1995]
当嵌套容器的关联性没有在模型中反映出来时,公共行为必然会在层次结构的每一层重复出现,而且嵌套也变得僵化(例如,容器通常不能包含同一层中的其他容器,而且嵌套的层数也是固定的)。客户必须通过不同的接口来处理层次结构中的不同层,尽管这些层在概念上可能没有区别。通过层次结构来递归地收集信息也变得非常复杂。
因此:定义一个把COMPOSITE的所有成员都包含在内的抽象类型。在容器上实现那些查询信息的方法时,这些方法返回由容器内容所汇总的信息。而“叶”节点则基于它们自己的值来实现这些方法。客户只需使用抽象类型,而无需区分“叶”和容器。
【学习心得】:很多时候,技术人员钉子思维是无法区分技术角度和模型角度。虽然许多方法是相通的,但不通维度的思考方式也会产生巨大的效果。学以致用,不是停留在嘴巴上,是在实践中证明的。
第13章:通过重构得到更深层次的理解
通过重构得到更深层次的理解是一个涉及很多方面的过程。我们有必要暂停一下,把一些要点归纳到一起。有三件事情是必须要关注的:
(1)以领域为本;
(2)用一种不同的方式来看待事物;
(3)始终坚持与领域专家对话。
在寻求理解领域的过程中,可以发现更广泛的重构机会。但一提到传统意义上的重构,我们头脑中就会出现这样一幅场景:一两位开发人员坐在键盘前面,发现一些代码可以改进,然后立刻动手修改代码(当然还要用单元测试来验证结果)。这个过程应该是一直进行下去,但它并不是重构过程的全部。
1、开始重构
与传统重构观点不同的是,即使在代码看上去很整洁的时候也可能需要重构,原因是模型的语言没有与领域专家保持一致,或者新需求不能被自然地添加到模型中。重构的原因也可能来自学习:当开发人员通过学习获得了更深刻的理解,从而发现了一个得到更清晰或更有用的模型的机会。
2、探索团队
不管问题的根源是什么,下一步都是要找到一种能够使模型表达变得更清楚和更自然的改进方案。这可能只需要做一些简单、明显的修改,只需几小时即可完成。在这种情况下,所做的修改类似于传统重构。但寻找新模型可能需要更多时间,而且需要更多人参与。
修改的发起者会挑选几位开发人员一起工作,这些开发人员应该擅长思考该类问题,了解领域,或者掌握深厚的建模技巧。如果涉及一些难以捉摸的问题,他们还要请一位领域专家加入。想要保证重构迭代过程的效率,需要注意几个关键事项:自主决定,注意范围和休息,以及练习使用UBIQUITOUS LANGUAGE。
3、借鉴先前的经验
我们没有必要总去做一些无谓的重复工作。用于查找缺失概念或改进模型的头脑风暴过程具有巨大的作用,通过这个过程可以收集来自各个方面的想法,并把这些想法与已有知识结合起来。随着知识消化的不断开展,就能找到当前问题的答案。
4、针对开发人员的设计
软件不仅仅是为用户提供的,也是为开发人员提供的。开发人员必须把他们编写的代码与系统的其他部分集成到一起。在迭代过程中,开发人员反复修改代码。开发人员应该通过重构得到更深层的理解,这样既能够实现柔性设计,也能够从这样一个设计中获益。
5、重构的时机
如果一直等到完全证明了修改的合理性之后才去修改,那么可能要等待太长时间了。项目正承受巨大的耗支,推迟修改将使修改变得更难执行,因为要修改的代码已经变得更加复杂,并更深地嵌入到其他代码中。持续重构渐渐被认为是一种“最佳实践”,但大不部分团队仍然对它抱有很大的戒心。
在探索领域的过程中,在培训开发人员的过程中,以及在开发人员与领域专家进行思想交流的过程中,必须始终坚持把“通过重构得到更深层次理解”作为这些工作的一部分。因此,当发生一下情况时,就应该进行重构了:
□ 设计没有表达出团队对领域的最新理解;
□ 重要的概念被隐藏在设计中了(而且你已经发现了把它们呈现出来的方法);
□ 发现了一个能令某个重要的设计部分变得更灵活的机会。
6、危机就是机遇
传统意义上的重构听起来是一个非常稳定的过程。但通过重构得到更深层理解往往不是这样的。在对模型进行一段时间稳定的改进后,你可能突然有所顿悟,而这会改变模型中的一切。这些突破不会每天都发生,然而很大一部分深层模型和柔性设计都来自这些突破。
这样的情况往往看起来不像是机遇,而更像危机。例如,你突然发现模型中一些明显的缺陷,在表达方面显示出一个很大的漏洞,或存在一些没有表达清楚的关键区域。或者有些描述是完全错误的。这些都表明团队对模型的理解已经达到了一个新的水平。他们现在站在更高的层次上发现了原有模型的弱点。他们可以从这种角度构思一个更好的模型。
【学习心得】:我曾几何时一直认为,发现自己问题是一种耻辱。这种思维极其可怕,当我不再发现自己问题的时候,那才叫可怕。在软件领域,新思维的提升叫重构,在生活方面,新观念的形成叫重生。