Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1721097
  • 博文数量: 177
  • 博客积分: 9416
  • 博客等级: 中将
  • 技术积分: 2513
  • 用 户 组: 普通用户
  • 注册时间: 2006-01-06 16:08
文章分类

全部博文(177)

文章存档

2013年(4)

2012年(13)

2011年(9)

2010年(71)

2009年(12)

2008年(11)

2007年(32)

2006年(25)

分类:

2007-12-14 09:44:14

本文译自Kent Beck在DDJ上的文章。

Kent Beck is the author of Implementation Patterns, from which this article is adapted. Copyright (c) 2008 Pearson Education. All rights reserved.

无论多么详尽的模式列表都无法覆盖到编程中遇到的每种情况。最终(甚至非常频繁),你将遇到一种情况,没有任何一种甜饼切割工具适合要求。用通用的方法来解决独特的问题这种需要就是学习编程理论的一个原因。另一个原因就是掌握某种事物的感觉来自于知道做什么以及为什么这么做。关于编程的谈话也会因为即谈及理论又涉及实践而变得有趣。

每个模式都传达了一点理论。然而,在编程工作中有着比单个模式覆盖更大且更为广泛的力量。本部分描述了这些截面的关注。它们被分成两种:值和原则。值是普遍的编程主旋律。当我工作做的不错时,我保持着对人际沟通重要性的珍惜,从我的代码里剔除不必要的复杂性,并保持我的选择是开放的。这些值——沟通,简单性,灵活性——使得我再编程中做的每个决定都充满色彩。

这里描述的原则不像值那样遥远或渗入人心的,但许多模式都表达了这里的每个原则。原则在值之间架起桥梁,虽然是通用的但却很难直接应用,而模式能被很清晰地应用却具有特定性。我发现对于那些没有模式可用,或者当两种互斥的模式同等可用的情况,将原则变得明确会非常有价值。当面临不明确的东西是,对原则的理解允许我“拼凑”一些与我实践的剩余部分一致且很可能有好结果的东西。这三种元素——值、原则、模式——组成了一个开发风格的和谐表达。模式描述了要做什么。值提供了动机。原则帮助将动机转换成行动。这里的值、原则、模式是从我自己的实践、反思以及与其他程序员的谈话中得出的。这些都是我们从上一代程序员的经验那里得出的。结果就是一种开发风格,而不是全部的开发风格。不同的值和不同的原则会得出不同的风格。将编程风格分为值、原则、实践的一个优势就是,用这种方式编程很容易让冲突表现出来。如果你想用这种方式,而我用另一种,我们就能识别不一致的级别并避免浪费时间。如果我们对于原则不一致,争论关于花括号属于哪里解决不了内在的不和谐。

三个与卓越编程一致的值是:沟通、简单性,灵活性。虽然有时这三者会有冲突,但更多的时候它们是互相补充的。最好的程序为未来的扩展提供了更多的选择,不包含无关的元素,而且容易阅读和理解。

沟通

当一个读者能够理解,修改,或使用代码时,代码沟通得更好。当编程时它试图仅仅从计算机角度考虑。然而,当我从其他人角度来考虑时,总会有好的事情发生。我得到更加整洁的代码,更划算,我的想法更清晰,我给自己一个新的观点,我的压力减小了,而且我有时间对付生活的需要。第一个把我吸引到编程中来的一部分原因就是与我之外的人、物交流的机会。然而,我不想对付难缠的、讨厌的人。没有沟通,变成就像身边没有人存在,直到几十年之后,在脑中构建更加精心制作的糖城堡变得毫无色彩,而且乏味。

早期的一次经历,阅读Knuth的Literate Programming,让我把焦点转向沟通:程序应该读起来像本书,要有情节,节奏,以及令人愉悦的短语。当Ward Cunningham和我第一次阅读文学性的程序,我们决定试试。我们拿着Smalltalk图形部分最整洁的一段代码——ScrollController——坐在一起,试图将其变成一个故事。几个小时之后我们完成了,将这段代码用我们自己的方式重写成合情合理的论文。每次遇到解释起来有点难度的逻辑,重写它比解释为什么它难以理解要简单。对于沟通的需要改变了我们编码的视点。

编程时将重点放在沟通上有一个听起来很经济的基础。软件最大的费用发生在第一次被部署之后。想想我修改代码的经验:我意识到我花在阅读已有代码上的时间比我写新代码的时间要多。如果我想让我的代码便宜,因此,我就要让它易读。

将焦点放在沟通上将通过更加现实的方式改进思考。一部分改进来自于我的大脑的更多部分被调动起来。当我思考“别人会怎么看?”时,比我只关注自己和计算机时更多的神经元被调动起来。我从我自己被隔离的视点里退后一步,重新观察我的问题和解决方法。另一部分的改进来自于因为小心处理业务而减少的压力。最后,作为一个具有社会性的物种,明确地把社会问题考虑进来要比假装不知道它们的存在要现实的多。

简单性

在The Visual Display of Quantitative Information中,Edward Tufte有一个练习,取得一个图形,然后开始擦掉所有不增加信息的标记,最终的结果这个图变得新颖而且比原来的那个要更容易理解。

消除过渡的复杂性让那些阅读、使用、修改程序的人更快的理解程序。一些复杂性是本质的,精确地反映要解决问题的复杂性的。然后有些复杂性则是不必要的,可能是在我们想让程序运行起来留下的“爪印”。就是这种复杂性,导致软件可能无法正确运行,以及将来难以修改,把价值从我们的软件中“偷”走了。一部分编程工作就是回头看看你做的工作,并且把无用的东西剔走(原文:把小麦从谷壳里分出来)。

简单性从旁观者眼中可以看出来。对于熟悉工具的专家程序员来说很简单的程序,对于初学者来说可能是无与伦比的难。就像好的散文要考虑读者一样,好的程序也要考虑读者。给你的读者一些挑战是对的,但是太难就会失去他们。

计算在复杂性和简单化的波峰波谷中前进。大型机的架构变得越来越复杂,直到迷你计算机发展壮大。迷你计算机没有解决大型机的所有问题,但它被证明对于大多说应用来说这些问题不重要。编程语言也一样,在复杂性和简单化的波峰波谷中前进:C产生C++,C++产生JAVA——一个现在已经变得更复杂的语言。

追随简单性会激活创造性。JUnit就比它所替代的巨大测试工具要简单很多. JUnit产生了大量相似的东西,插件,以及新的编程/测试技术。最新的版本JUnit 4,已经失去了“bare metal”的感觉,虽然我制造了或者帮助增加了每个复杂化的决定。某天某人会想出一个比JUnit更简单写测试的方法。新的想法会激活未来创新的波浪。

在所有的层次上应用简单性。格式化代码使得信息不会丢失而且再没有代码能被删除。不用无关的元素来设计。挑战需求以发现那些本质东西。消除不必要的复杂性使剩余的代码更加靓丽,给你一种完成它的全新方法。

沟通和简单性经常一起工作。越少的复杂性,系统就越容易理解。你越把焦点放在沟通上,就越容易发现那些复杂性可以被丢弃。然而,有时候我发现简单化可能会把一个程序变得难以理解。在这些情况中我选择对简单性进行沟通。这样的情况比较少见但是,通常都表明需要一些我还没有认识到的大规模简化。

灵活性

在列出的这三个值中,灵活性可以用来判断大多数效率低下的编码和设计。为了获取一个常量,我发现很多程序查找包含路径名的环境变量已获得含有该常量的文件。为什么会有这么大的复杂性?灵活性带来的。程序必须灵活,但仅仅在它们改变的方式上。如果常量从来不改变,所有的那些复杂性得不偿失。

由于大多数程序上的花费都是在它第一次部署之后发生的,因此它应该是易于改变的。我认为可能以后才需要灵活性,当我改变代码是可能就不是我想要的。这就是为什么投机式的设计提供的灵活性不如简单性和可扩展性提供的灵活性。

选择那些能够立即带来效益并且鼓励灵活性的模式。对于那些能立即带来花销且只能得到延缓的效益的模式,最好的策略就是多点耐心。把它们放回书包里,直到需要用时再去出来。这样你可以按照它们所应当的那种方式应用它。

灵活性会随着增长的复杂性带来的花销而来。例如,用户可配的选项提供了灵活性,增加了配置文件的复杂性,并且在编程的时候需要考虑选项的处理。简单性能鼓励灵活性。在上面的例子中,如果你能找到一种不丢失值而消除配置选项的方法,程序就会变得易于修改。

增强软件的沟通性也会增加灵活性。越多的人能快速阅读、理解、修改代码,你的组织获得未来的修改的选择越多。遵循鼓励灵活性的模式能帮助程序员写出简单、可理解的应用。

原则

实现模式不是仅仅因为“就应该这样”。每一个都表达了沟通、简单性、灵活性三者的一个或者更多。原则是一般性思想的另一个等级,比值更加贴近编程,也组成了模式的基础。

从多种角度来讲,检查原则是有价值的。清晰的原则可以导出新的模式,就像元素周期表导出新元素的发现。原则能够揭示模式背后的动机,这个动机是与更加通用的想法关联的。最后,对原则的理解将在遇到一场情况时提供指导。

例如,当我碰到一种新的编程语言,我利用我对原则的理解来开发一种有效的编程风格。我不必使用甚至更糟——依赖其他编程语言的风格。对原则的理解让我有机会快速学习并在遇到异常情况时采取措施。遵循的是这些模式背后的原则。

局部性

构造代码以使改变具有局部性。如果在这里的一个改变会导致那里的问题,那么改变带来的花销就会急剧增加。基本上具有局部性的代码能够有效地沟通。这是可以理解的,因为你不用对整体有了解。

因为实现模式背后首要的动机就是让改动的代价保持一个低水平,局部性原则成为许多模式背后的推理的一部分。

最小化重复

对于保持局部性作出贡献的一个原则就是最小化重复。当很多地方都有相同的代码时,你改变其中一个拷贝就必须考虑其他的是否也需要修改。你的改变就不再是局部的了。同一段代码出现的次数越多,改变的花费就越大。

拷贝的代码仅仅是重复的一种表现。平行的类层次也是重复的,打破了局部性原则。如果一个概念上的改变会导致要更改两个或者更多的类层次,那么改变就具有广泛性。重构代码使之变得局部化会改进代码质量。

重复并不总是很明显,而且有些要过一段时间才能看出来。一旦看见我总是不能想到一个消除它的好办法。重复不是魔鬼,它只是让修改的代价升高。

移除重复的一个方法就是将程序分成很多小块——小的语句块,小的方法,小对象,小包。大段的逻辑容易造成对其他大块逻辑的重复。这种共性让模式变得可能—— 不同的代码段之间有区别的同时也会有很多相同的地方。哪些部分是一样的,哪些部分相似,哪些部分完全不同,对这些问题进行清晰沟通之后,程序就会变得更容易阅读,修改的代价也越小。
把逻辑和数据放在一起

另一个从局部性推出的必然结果就是把数据和逻辑放在一起。将它们安排得尽可能进,如果可能就放在同一个方法中,或者同一个对象里,或者至少同一个包。要做修改,逻辑和数据很有可能需要同时修改。如果它们在一起,那么修改起来就是局部的。

把数据和逻辑放在哪里能够满足该原则,这个问题最初是不明显的。我可能在A写代码然后意识到我需要从B获得数据。这仅仅是发生在我让这些代码工作起来之后才注意到,逻辑与数据离得太远。然后我就需要选择应该做什么:将代码移到数据附近,将数据移到代码附近,将它们放到一起。

对称性

另一个我一直使用的原则是对称性。程序都充满了对称性。add()方法伴随着remove()方法。一组方法都使用相同的参数。一个对象的所有成员都有相同的生命期。识别并清晰地表达对称性让代码更容易阅读。一旦读者理解了对称性的一半,他们会很快理解另一半。

经常使用这些术语来讨论对称性:双面的,轮转的,等等。程序中的对称性很少是可以绘制成图形的,它是概念性的。代码中的对称性就是在代码中出现的任何地方,相同的思想用同一种方法来表达。

这里是缺乏对称性的一个例子:
void process() {
   input();
   count++;
   output();
}

第二条语句比另外两个消息更具有具体性。我更愿意用对称性来重写,得到:
void process() {
   input();
   incrementCount();
   output();
}

这个方法仍然未被了对称性。input()和output()操作用意图命名,而incrementCount()用实现来命名。查看一下对称性,我思考了一下为什么我要增加count,可能得到:
void process() {
   input();
   tally();
   output();
}

通常,发现和表达对称性是移除重复的第一步。如果一个相同的想法出现在代码中的很多地方,把它们做成对称的是同一它们的第一个好的步骤。

声明表达式

另一个在模式后面隐藏的原则是在声明里尽可能多的表达意图。祈使式的编程很强大也很灵活,但要阅读它需要你遵循执行的过程。我必须在我的头脑中构建一个包含程序状态,流程和数据的模型。对于程序那些更简单的部分,简单声明的代码要容易阅读。

例如,在JUnit的老版本中,类可以有静态的返回一系列要运行的测试的suite()方法。
public static junit.framework.Test suite() {
   Test result= new TestSuite();
   ...complicated stuff...
   return result;
}

现在有一个简单、普遍的问题——什么样的测试将被运行?在大多数情况下,suite()方法仅仅是把一系列的测试聚集到一些类中。然而,由于suite()方法是通用的,如果我想确认,我不得不阅读并理解这个方法。另一方面,JUnit 4使用声明表达式来解决相同的问题。与返回一套要运行的测试相反,有一个特殊的测试运行器来运行一系列类的测试(通常情况):
@RunWith(Suite.class)
@TestClasses({
   SimpleTest.class,
   ComplicatedTest.class
})

class AllTests {
}

如果我知道使用这个方法会聚集一些测试,我只需要观察TestClasses的注解来看看什么样的测试将被执行,suite的表达式是声明式的我就不需要考虑任何技巧性的例外。这个解决方案放弃了强大的 能力和原始suite()方法的通用性,但声明式风格让代码易读。(RunWith注解为运行测试提供了比suite()更大的灵活性,但这是另外一个话题。)

修改的比率

最后一个原则是把相同的修改比率的逻辑和数据放到一起,并把不同修改比率的逻辑和数据分开。这些修改比率是暂时对称性的一种表现形式。有时修改比率原则可以应用到一个程序员做的修改。例如,我在写税收软件时我就会把产生通用税计算的代码从某年的税收计算代码中分离出来。因为代码改变的比率不一样。当我下一年做修改是,我可能会确信前些年的代码仍然工作。把它们分开让我对我的修改的局部性更有信心。

修改比率也可应用于数据。单个对象的所有数据应当按照同样的比率来改变。例如,在某个方法激活期间修改的数据应当是局部变量。不同步改变的两个数据成员以及它们的邻居成员可能属于某个辅助 对象。如果一个金融手段能够让自己的值和汇率一起改变,那么这两个成员由一个辅助对象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是对称的。它们同时改变。然而,它们与其他的成员一起并不是对称的。通过将它们放在一起来表达对称性向读者表明他们之间的关系,而且可能为将来设置了更多的降低重复,增加局部性的机会。

PS:这篇文章原文比较难懂,感觉有点散文风格。于是决定将其翻译成中文。没想到不光翻译难,翻译完之后理解起来同样也很困难。不过看了几遍译文之后,总算搞明白了。
为了翻译、理解,我花了整整一个晚上,不过确实学到了东西,也算没白花时间。总结起来就是:1)写代码时要多考虑人而不是机器——将代码写成故事、散文;2)简单就是美;3)程序应当具备灵活性。这三者可以通过1)局部化;2)对称化;3)重复最小化;4)关注变化的速率(比率)来得到。

阅读(2009) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~