第3章 一种编程理论
实现模式(修订版)
就算是再巨细靡遗的模式列表,也不可能涵盖编程中所遇到的每一种情况。你免不了(甚至常常)会遭遇到这种情景:上穷碧落,也找不到对应的现成解决方案。于是便需要有针对特定问题的通用解决方案。这也正是学习编程理论的原因之一。原因之二则是那种知晓如何去做、为何去做之后所带来的胸有成竹。当然,如果把编程的理论和实践结合起来讨论,内容就会更加精彩了。
每个模式都承载着一点点理论。但实际编程中存在一些更加深广的影响力,远不是孤立的模式所能概括的。本章将会讲述这些贯穿于编程中的横切概念,它们分为两类:价值观与原则。价值观是编程过程的统一支配性主题。珍视与其他人沟通的重要性,把代码中多余的复杂性去掉,并保持开放的心态,这才是我工作状态最佳的表现。这些价值观——沟通、简单和灵活——影响了我在编程时所做的每个决策。
此处描述的原则不像上面的价值观那样意义深远,不过每一项原则都在许多模式中得以体现。价值观有普遍的意义,但往往难以直接应用;模式虽可以直接应用,却是针对于特定情景;原则在价值观和模式之间搭建了桥梁。我早已发现,在那种没有模式可以应用,或是两个相互排斥的模式可以同等应用的场合,如果把编程原则弄清楚,对解决疑难会是一件好事。在面对不确定性的时候,对原则的理解让我可以“无中生有”创造出一些东西,同时能和其他的实践保持一致,而且结果一般都不错。
价值观、原则和模式,这3种元素互为补充,组成了一种稳定的开发方式。模式描述了要做什么,价值观提供了动机,原则把动机转化成了实际行动。
这里的价值观、原则和模式,是通过我的亲身实践、反思以及与其他人的讨论总结出来的。我们都曾经从前人那里吸收经验,最终会形成一种开发方式,但不是唯一的开发方式。不同的价值观和不同的原则会产生不同的方式。把编程方式用价值观、原则和模式的形式展现出来,其优点之一就是可以更加有效地展现编程方法的差异。如果你喜欢用某种方式来做事,而我喜欢另一种,那么就可以识别出我们在哪种层次上存在分歧,从而避免浪费时间。如果我们各自认可不同的原则,那么争论哪里该用大括号根本无助于解决问题。
3.1 价值观
有3个价值观与卓越的编程血脉相连,它们分别是:沟通、简单和灵活。虽然它们有时候会有所冲突,但更多的时候则是相得益彰。最优秀的程序会为未来的扩展留下充分的选择余地,不包含不相关的元素,容易阅读,容易理解。
3.1.1 沟通
如果阅读者可以理解某段代码,并且进一步修改或使用它,那么这段代码的沟通效果就很好。在编程时,我们很容易从计算机的角度进行思考。但只有一面编程一面考虑其他人的感受,我才能编写出好的代码。在这种前提下编写出的代码更加干净易读,更有效率,更清晰地展现出我的想法,给了我全新的视角,减轻了我的压力。我的一些社会性需要得到了自我满足。最开始编程吸引我的部分原因在于我可以通过编程与外界交流,然而,我不想与那些难缠又无法理喻的烦人家伙打交道。过了20年,把别人当作空气一样的编程方式才在我眼中褪尽了颜色。耗尽心神去精心搭建一座糖果城堡,于我而言已毫无意义。
Knuth所提出的文学编程理论促使我把注意力放到沟通上来:程序应该读起来像一本书一样。它需要有情节和韵律,句子间应该有优雅的小小跌宕起伏。
我和Ward Cunningham第一次接触到文学性程序这个概念以后,我们决定来试一试。我们找出Smalltalk中最干净的代码之一——ScrollController,坐到一起,然后试着把它写成一个故事。几个小时以后,我们以自己的方式完全重写了这段代码,把它变成了一篇合情合理的文章。每次遇到难以解释清楚的逻辑,重新把它写一遍都要比解释这段代码为何难以理解容易得多。沟通的需要改变了我们对于编码的看法。
在编程时注重沟通还有一个很明显的经济学基础。软件的绝大部分成本都是在第一次部署以后才产生的。从我自己修改代码的经验出发,我花在阅读既有代码上的时间要比编写全新的代码长得多。如果我想减少代码所带来的开销,我就应该让它容易读懂。
注重沟通还可以帮助我们改进思想,让它更加现实。一方面是由于投入更多的思考,考虑“如果别人看到这段代码会怎么想”所需要调动的脑细胞,和只关注自己是不一样的。这时我会退后一步,从全新的视角来审视面对的问题和解决方案。另一方面则是由于压力的减轻,因为我知道自己所做的事情是在务正业,我做的是对的。最后,作为社会性的产物,明确地考虑社会因素要比在假设它们不存在的情况下工作更为现实。
3.1.2 简单
在Visual Display of Quantitative Information一书中,Edward Tufte做过一个实验,他拿过一张图,把上面没有增加任何信息的标记全都擦掉,最终得到了一张很新颖的图,比原先那张更容易理解。
去掉多余的复杂性可以让那些阅读、使用和修改代码的人更容易理解。有些复杂性是内在的,它们准确地反映出所要解决的问题的复杂性。但有些复杂性的产生完全是因为我们忙着让程序运行起来,在摆弄过程中留下来的“指甲印”没擦干净。这种多余的复杂性降低了软件的价值,因为一方面软件正确运行的可能性降低了,另一方面将来也很难进行正确的改动。回顾自己做过的事情,把麦子和糠分开,是编程中不可或缺的一部分。
简单存在于旁观者的眼中。一个可以将专业工具使用得得心应手的高级程序员,他所认为的简单事情对一个初学者来说可能会比登天还难。只有把读者放在心里,你才可以写出动人的散文。同样,只有把读者放在心里,你才可以编写出优美的程序。给阅读者一点挑战没有关系,但过多的复杂性会让你失去他们。
在复杂与简单的波动中,计算机技术不断向前推进。直到微型计算机出现之前,大型机架构的发展倾向仍然是越来越复杂。微型计算机并没有解决大型机的所有问题,只不过在很多应用中,那些问题已经变得不再重要。编程语言也在复杂和简单的起伏中前行。C++在C的基础上产生,而后在C++的基础上又出现了Java,现在Java本身也变得越来越复杂了。
追求简单推动了进化。JUnit比它所大规模取代的上一代测试工具简单得多。JUnit催生了各种模仿者、扩展软件和新的编程/测试技术。它最近一个版本JUnit 4已经失去了那种“一目了然”的效果,虽然每一个导致其复杂化的决定都有我参与其中,但亦未能阻止这种趋势。总有一天,会有人发明一种比JUnit简单许多的方式,以方便编程人员编写测试。这种新的想法又会推动另一轮进化。
在各个层次上都应当要求简单。对代码进行调整,删除所有不提供信息的代码。设计中不出现无关元素。对需求提出质疑,找出最本质的概念。去掉多余的复杂性后,就好像有一束光照亮了余下的代码,你就有机会用全新的视角来处理它们。
沟通和简单通常都是不可分割的。多余的复杂性越少,系统就越容易理解;在沟通方面投入越多,就越容易发现应该被抛弃的复杂性。不过有时候我也会发现某种简化会使程序难以理解,这种情况下我会优先考虑沟通。这样的情形很少,但常常都表示这里应该有一些我尚未察觉的更大规模的简化。
3.1.3 灵活
在三种价值观中,灵活是衡量那些低效编码与设计实践的一把标尺。以获取一个常量为例,我曾经见到有人会用环境变量保存一个目录名,而那个目录下放着一个文件,文件中写着那个常量的值。为什么弄这么复杂?为了灵活。程序是应该灵活,但只有在发生变化的时候才需如此。如果这个常量永远不会变化,那么付出的代价就都白费了。
因为程序的绝大部分开销都是在它第一次部署以后才产生,所以程序必须要容易改动。想象中明天或许会用得上的灵活性,可能与真正修改代码时所需要的灵活性不是一回事。这就是简单性和大规模测试所带来的灵活性比专门设计出来的灵活性更为有效的原因。
要选择那些提倡灵活性并能够带来及时收益的模式。对于会立刻增加成本但收效却缓慢的模式,最好让自己多一点耐心,先把它们放回口袋里,需要的时候再拿出来。这样就可以用最恰当的方式使用它们。
灵活性的提高可能以复杂性的提高为代价。比如说,给用户提供一个可自定义配置的选择提高了灵活性,但是因为多了一个配置文件,编程时也需要考虑这一点,所以也就更复杂了。反过来简单也可以促进灵活。在前面的例子中,如果可以找到取消配置选项但又不丧失价值的方式,那么这个程序以后就更容易改动。
增进软件的沟通效果同样会提高灵活性。能够快速阅读、理解和修改你的代码的人越多,它将来发生变化的选择就越多。
本书中介绍的模式会通过帮助编程人员创建简单、可以理解、可以修改的应用程序来提高程序的灵活性。