本文共 3586 字,大约阅读时间需要 11 分钟。
本节书摘来自异步社区《实现模式(修订版)》一书中的第3章3.2节原则,作者【美】Kent Beck,更多章节内容可以访问云栖社区“异步社区”公众号查看。
3.2 原则
实现模式并不是无缘无故产生的。每一种模式都或多或少体现了沟通、简单和灵活这些价值观。原则是另一个层次上的通用思想,比价值观更贴近于编程实际,同时又是模式的基础。我们有很多理由来检查一下这些原则。正如元素周期表帮助人们发现了新的元素,清晰的原则也可以引出新的模式。原则可以解释模式背后的动机,它是有普遍意义的。在对立模式间进行选择时,最好的方式就是用原则来说话,而不是让模式争来争去。最后,如果遇到从未碰到过的情况,对原则的理解可以充当我们的向导。
例如,假如要使用新的编程语言,我可以根据自己对原则的理解发展出有效的编程方式,不必盲目模仿现有的编程方式,更不用拘泥于在其他语言中形成的习惯(虽然可以用任何语言编写FORTRAN风格的代码,但不该那么做)。对原则的充分理解使我能够快速地学习,即使在新鲜局面下仍然能够一以贯之地符合原则。接下来的部分,我将为你讲述隐藏在模式背后的原则。
3.2.1 局部化影响
组织代码结构时,要保证变化只会产生局部化影响。如果这里的一个变化会引出那里的一个问题,那么变化的代价就会急剧上升了。把影响范围缩到最小,代码就会有极佳的沟通效果。它可以被逐步深入理解,不必一开始就要鸟瞰全景。因为实现模式背后一条最主要的动机就是减少变化所引起的代价,所以局部化影响这条原则也是很多模式的形成缘由之一。3.2.2 最小化重复
最小化重复这条原则有助于保证局部化影响。如果相同的代码出现在很多地方,那么改动其中一处副本时,就不得不考虑是否需要修改其他副本;变动不再只发生在局部。代码的复制越多,变化的代价就越大。复制代码只是重复的一种形式。并行的类层次结构也是其一,同样破坏了局部化影响原则。如果修改一处概念需要修改两个或更多的类层次结构,就表示变化的影响已经扩散了。此时应重新组织代码,让变化只对局部产生影响。这种做法可以有效改进代码质量。
重复不容易被预见到,有时在出现以后一段时间才会被觉察。重复不是罪过,它只是增加了变化的开销。
我们可以把程序拆分成许多更小的部分——小段语句、小段方法、小型对象和小型包,从而消除重复。大段逻辑很容易与其他大段逻辑出现重复的代码片断,于是就有了模式诞生的可能,虽然不同的代码段落中存在差异,但也有很多相似之处。如果能够清晰地表述出哪些部分程序是等同的,哪些部分相似性很少,而哪些部分则截然不同,程序就会更容易阅读,修改的代价也会更小。
3.2.3 将逻辑与数据捆绑
局部化影响的必然结果就是将逻辑与数据捆绑。把逻辑与逻辑所处理的数据放在一起,如果有可能尽量放到一个方法中,或者退一步,放到一个对象里面,最起码也要放到一个包下面。在发生变化时,逻辑和数据很可能会同时被改动。如果它们被放在一起,那么修改它们所造成的影响就会只停留在局部。在编码开始的那一刻,我们往往不太清楚该把逻辑和数据放到哪里。我可能在A中编写代码的时候才意识到需要B中的数据。在代码正常工作之后,我才意识到它与数据离得太远。这时候我需要做出选择:是该把代码挪到数据那边去,还是把代码挪到逻辑这边来,或者把代码和数据都放到一个辅助对象中?也许还可能意识到,这时我还没法找出如何组合它们以便增进沟通的最好方式。
3.2.4 对称性
对称性也是我随时随地运用的一项原则。程序中处处充满了对称性。比如add()方法总会伴随着remove()方法,一组方法会接受同样的参数,一个对象中所有的字段都具有相同的生命周期。识别出对称性,把它清晰地表述出来,代码将更容易阅读。一旦阅读者理解了对称性所涵盖的某一半,他们就会很快地理解另外一半。对称性往往用空间词汇进行表述:左右对称的、旋转的,等等。程序中的对称性指的是概念上的对称,而不是图形上的对称。代码中对称性的表现,是无论在什么地方,同样的概念都以同样的形式呈现。
这是一个缺少对称性的例子:
void process() { input(); count++; output();}
第二条语句比其他的语句更加具体。我会根据对称性的原则重写它,结果是:
void process() { input(); incrementCount(); output();}
这个方法依然违反了对称性。这里的input()和output()操作都是通过方法意图来命名的,但是incrementCount()这个方法却以实现方式来命名。在追求对称性的时候,我会考虑为什么我会增加这个数值,于是就有了下面的结果:
void process() { input(); tally(); output();}
在准备消灭重复之前,常常需要寻找并表示出代码中的对称性。如果在很多代码中都存在类似的想法,那么可以先把它们用对称的方式表示出来,让接下来的重构有一个良好开端。
3.2.5 声明式表达
实现模式背后的另一条原则是尽可能声明式地表达出意图。命令式的编程语言功能强大灵活,但是在阅读时需要跟随着代码的执行流程。我必须在大脑中建起一个程序状态、控制流和数据流的模型。对于那些只是陈述简单事实,不需要一系列条件语句的程序片断,如果用简单的声明方式写出来,读着就容易多了。比如在JUnit的早期版本中,测试类里可能会有一个静态的suite()方法,该方法会返回需要运行的测试集合。
public static junit.framework.Test suite() { Test result= new TestSuite(); ...complicated stuff... return result;}
现在就有了一个很简单很常见的问题:哪些测试会被执行?在大多数情况下,suite()方法只是将多个类中的测试汇总起来。但是因为它是一个通用方法,所以我必须要读过、理解该方法以后,才能够百分之百确定它的功能。
JUnit 4用了声明式表达原则来解决这个问题。它不是用一个方法来返回测试集,而是用了一个特殊的test runner来执行多个类中的所有测试(这是最常见的情况):
@RunWith(Suite.class)@TestClasses({ SimpleTest.class, ComplicatedTest.class})class AllTests {}
如果测试是用这种方式汇总的,那么我只需要读一下TestClasses注解就可以知道哪些测试会被执行。面对这种声明式的表达方式,我不需要臆测会出现什么奇怪的例外情况。这个解决方案放弃了原始的suite()方法所具备的能力和通用性,但是它声明式的风格使得代码更加容易阅读。(在运行测试方面,RunWith注解比suite()方法更为灵活,但这应该是另外一本书里的故事了。)
3.2.6 变化率
最后一个原则就是把具有相同变化率的逻辑、数据放在一起,把具有不同变化率的逻辑、数据分离。变化率具有时间上的对称性。有时候可以将变化率原则应用于人为的变化。例如,如果开发一套税务软件,我会把计算通用税金的代码和计算某年特定税金的代码分离开。两类代码的变化率是不同的。在下一年中做调整的时候,我会希望能够确保上一年中的代码依然奏效。分离两类代码可以让我更确信每年的修改只会产生局部化影响。变化率原则也适用于数据。一个对象中所有成员变量的变化率应该差不多是相同的。只会在一个方法的生命周期内修改的成员变量应该是局部变量。两个同时变化但又和其他成员的变化步调不一致的变量可能应该属于某个辅助对象。比如金融票据的数值与币种会同时变化,那么这两个字段最好放到一个辅助对象Money中:
setAmount(int value, String currency) { this.value= value; this.currency= currency;}
上面这段代码就变成了:
setAmount(int value, String currency) { this.value= new Money(value, currency);}
然后进一步调整:
setAmount(Money value) { this.value= value; }
变化率原则也是对称性的一个应用,不过是时间上的对称。在上面的例子中,value和currency这两个初始字段是对称的,它们会同时变化。但它们与对象中其他的字段是不对称的。把它们放到自己应该从属的对象中,让新的对象向阅读者传达出它们的对称关系,这样就更有可能在将来消除重复,进一步达到影响的局部化。
转载地址:http://qfdux.baihongyu.com/