实现模式(修订版)

978-7-115-29460-9
作者: 【美】Kent Beck
译者: 李剑熊节郭晓刚
编辑: 杨海玲

图书目录:

详情

作者将自己多年形成的编程习惯和阅读既有代码的体验凝练成了编程中的价值观、原则和77种实现模式。在77个实现模式中,每一个模式都覆盖了编写简洁、清晰、易扩展、易维护的代码这一原则的某个方面,为日常编程提供了丰富翔实的参考依据。

图书摘要

版权信息

书名:实现模式(修订版)

ISBN:978-7-115-29460-9

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Kent Beck

  译    李 剑 熊 节 郭晓刚

  责任编辑 杨海玲

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Authorized translation from the English language edition, entitled Implementation Patterns, 9780321413093 by Kent Beck, published by Pearson Education, Inc, publishing as Addison-Wesley, Copyright © 2008 Pearson Education, Inc.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD., and POSTS & TELECOMMUNICATIONS PRESS Copyright © 2009.

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签。无标签者不得销售。


在本书中,作者将自己多年形成的编程习惯以及阅读既有代码的体验凝练成了编程中的价值观、原则和77种实现模式。

沟通、简单和灵活的价值观应当被所有开发人员所铭记。局部影响、最小化重复、将逻辑与数据捆绑等原则同样是通用性的指导思想,比价值观更贴近编程场景,在价值观和模式之间搭建了桥梁。在77种实现模式中,每一种模式都覆盖了编写简洁、清晰、易扩展、易维护的代码这一原则的某个方面。它们为日常的编程提供了丰富翔实的参考依据,并告诉大家这些代码如何为降低沟通成本和提高有效产出提供保障。

本书适用于各个阶段的开发者群体。刚刚涉足软件开发领域的新人能够透过大师的眼睛来看待编程,了解编程的价值观与原则;具有丰富经验的资深工程师则可以通过这些模式进行反思,探究成功实践背后的意义。把价值观、原则和开发实践结合之后,日常开发工作便会以崭新而迷人的形式呈现在我们面前。



这是一本关于如何写好代码的书。

如果你不认为写好代码是一件重要、困难并且有趣的事,请立即放下这本书。

什么是好的代码?可以工作的、性能良好的、不出bug的代码,就是好的代码吗?

所谓好的代码,除了其他所有要求以外,还应该清晰准确地传达写作者的想法。

Martin Fowler在《重构:改善既有代码的设计》里说,“任何一个傻瓜都能写出机器能懂的代码。好的程序员应该写出人能懂的代码。”

如果你不同意这句话,请立即放下这本书。因为这是一本关于如何用代码与他人(而非机器)沟通的书。

任何读到这一行的程序员都应该读完这本书。

Steve McConnell在《代码大全》里说,“不要过早优化,但也不要过早劣化”。这本书将告诉你如何在几乎不引入任何额外成本的前提下避免一些常见的低级错误——它们是常见的,因为几乎每个人都犯过并且还在犯着这些错误。

如果你确实没有时间,至少应该读完第6章“状态”。因为在各种常见的低级错误中最常见的就是关于“什么信息在什么地方”的决策错误。

在这样一本书的序言里说任何废话都将是佛头着粪。

所以,现在就祝你阅读愉快、编程愉快。

是为序。


这是一本关于编程的书,更具体一点,是关于“如何编写别人能懂的代码”的书。编写出别人能读懂的代码没有任何神奇之处,这就与任何其他形式的写作一样:了解你的阅读者,在脑子里构想一个清晰的整体结构,让每个细节为故事的整体作出贡献。Java提供了一些很好的交流机制,本书介绍的实现模式正是一些Java编程习惯,它们能让你编写出的代码更加易读。

也可以把实现模式看作思考“关于这段代码,我想要告诉阅读者什么?”的一种方式。程序员大部分的时间都在自己的世界里绞尽脑汁,以至于用别人的视角来看待世界对他们来说是一次重大的转变。他们不仅要考虑“计算机会用这段代码做什么”,还要考虑“如何用这段代码与别人沟通我的想法”。这种视角上的转换有利于你的健康,也很可能有利于你的钱包,因为在软件开发中有大量的开销都被用在理解现有代码上了。

有一个叫做Jeopardy的美国游戏节目,由主持人给出问题的答案,参赛观众则来猜问题是什么。“猜一个词,表示扔出窗外。”“是defenestration吗?”“答对了。”

编程就好像Jeopardy游戏:答案用Java的基本语言构造给出,程序员则经常需要找出问题究竟是什么,即这些语言构造究竟是在解决什么问题。比如说,如果答案是“把一个字段声明为Set”,那么问题可能就是“我要怎样告诉其他程序员,这是一个不允许包含重复元素的集合?”本书介绍的实现模式列举了一组常见的编程问题,还有Java解决这些问题的方式。

和软件开发一样,范围的管理对于写书同样重要。我现在就告诉你本书不是什么。它不是编程风格指南,因为其中包含了太多的解释,最终的决定权则完全交给你。它不是设计书籍,因为其中关注的主要是小范围的、程序员每天要做很多次的决策。它不是模式书籍,因为这些实现模式的格式各不相同、随需而变。它不是语言书籍,因为尽管其中谈到了一些Java语言的特性,但我在写作时假设你已经熟悉Java了。

实际上本书建立在一个相当不可靠的前提之上:好的代码是有意义的。我见过太多丑陋的代码给它们的主人赚着大把钞票,所以在我看来,软件要取得商业成功或者被广泛使用,“好的代码质量”既不必要也不充分。即便如此,我仍然相信,尽管代码质量不能保证美好的未来,但它仍然有其意义:有了质量良好的代码以后,业务需求能够被充满信心地开发和交付,软件用户能够及时调整方向以便应对机遇和竞争,开发团队能够在挑战和挫折面前保持高昂的斗志。总而言之,比起质量低劣、错误重重的代码,好的代码更有可能帮助用户取得业务上的成功。

即便不考虑长期的经济效应,我仍然会选择尽我所能地编写出好代码。就算活到古稀之年,你的一生也只有二十多亿秒而已,这宝贵的时间不该被浪费在自己不能引以为傲的工作上。编写好的代码带给我满足感,不仅因为编程本身,还因为知道别人能够理解、欣赏、使用和扩展我的工作。

所以,说到底,这是一本关于责任的书。作为一个程序员,你拥有时间、拥有才华、拥有金钱、拥有机会。对于这些天赐的礼物,你要如何负责地使用?下面的篇幅里包含了我对于这个问题的答案:不仅为我自己、为我的CPU老弟编程,也为其他人编程。


我首先、最后并且始终要感谢Cynthia Andres,我的搭档、编辑、支持者和首席讨债鬼。我的朋友Paul Petralia推动了这本书的写作,而且不断给我鼓励的电话。编辑Chris Guzikowski和我通过本书学会了如何在一起工作,他从Pearson的角度给了我一切需要的支持,让我能够完成写作。还要感谢Pearson的制作团队:Julie Nahil、John Fuller和Cynthia Kogut。Jennifer Kohnke的插图不但包含了丰富的信息,而且非常人性化。本书的审阅者给我的书稿提供了清晰而又及时的反馈,为此我要感谢Erich Gamma、Steve Metsker、Diomidis Spinellis、Tom deMarco、Michael Feathers、Doug Lea、Brad Abrams、Cliff Click、Pekka Abrahamson、Gregor Hohpe和Michele Marchesi。感谢David Saff指出“状态”与“行为”之间的平衡。最后,还要感谢我的孩子们,一想到他们乖乖呆在家里,我就有了尽快完成本书的动力。Lincoln、Lindsey、Forrest和Joëlle Andres,感谢你们。


现在开始吧。你选中了我的书(现在它就是你的了),你也编写过代码,很可能你已经从自己的经验中建立了一套属于自己的风格。

这本书的目标是要帮助你通过代码表达自己的意图。首先,我们对编程和模式做一个概述(第2章~第4章)。接下来(第5章~第8章)用一系列短文和模式,讲述了“如何用Java编写出可读的代码”。如果你正在编写框架(而不是应用程序),最后一章会告诉你如何调整前面给出的建议。总而言之,本书关注的焦点是用编程技巧来增进沟通。

用代码来沟通有几个步骤。首先,必须在编程时保持清醒。第一次开始记录实现模式时,我编程已经有一些年头了。我惊讶地发现,尽管能够快捷流畅地作出各种编程中的决定,但我没法解释自己为什么如此确定诸如“这个方法为什么应该被这样调用”或者“那块代码为什么属于那个对象”之类的事情。迈向沟通的第一步就是让自己慢下来,弄明白自己究竟想了些什么,不再假装自己是在凭本能编程。

第二步是要承认他人的重要性。我很早就发现编程是一件令人满足的事,但我是个以自我为中心的人,所以必须学会相信其他人也跟我一样重要,然后才能写出能与他人沟通的代码。编程很少会是一个人与一台机器之间孤独的交流,我们应该学会关心其他人,而这需要练习。

所以我迈出了第三步。一旦把自己的想法暴露在光天化日下,并且承认别人也有权和我一样地存在,我就必须实实在在地展示自己的新观点了。我使用本书中介绍的实现模式,目的是更有意识地编程,为他人编程,而不仅仅是为自己编程。

你当然可以仅为其中的技术内容——有用的技巧和解释——而阅读本书,但我认为应该预先提醒你,除了这些技术内容,本书还包含了很多东西,至少对我而言是这样。

这些技术内容都可以在介绍模式的章节里找到。学习这部分内容有一个高效的策略:需要用的时候再去读。如果用这种“just-in-time”的方式来读,那么可以直接跳到第5章,把后续的章节快速扫一遍,然后在编程时把本书放在手边。用过书中的很多模式之后,你可以重新回到前面介绍性的内容中来,读一下那些技巧背后的道理。如果有兴趣透彻理解手上的这本书,也可以细细地从头读到尾。和我写过的大部分书不同,这本书的每一章都相当长,因此在阅读时要保持专注才行。

书中的大部分内容都以模式的形式加以组织。编程中需要做的抉择大多曾经出现过。一个程序员的职业生涯中可能要给上百万个变量命名,不可能每次都用全新的方式来命名。命名的普遍约束总是一致的:需要把变量的用途、类型和生命周期告诉给阅读者,需要挑选一个容易读懂的名字,需要挑选一个容易写、符合标准格式的名字。把这些普遍约束加诸一个具体的变量之上,然后就得到了一个合用的名字。“给变量命名”就是一个模式:尽管每次都可能创造出不同的名字,但决策的方法和约束条件总是重复出现的。

我觉得,模式需要以多种形式来呈现,有时一篇充满争议的文章能最好地阐释一个模式,有时候是一幅图,有时候是一个故事,有时候是一段示例。所以我并没有尝试把所有模式都塞进同一种死板的格式里,而是以我认为最恰当的方式来描述它们。

书中总共包含了77个明确命名的模式,它们分别涵盖了“编写可读的代码”这件事的某一方面。此外我还在书中提到了很多更小的模式或是模式的变体。我写这本书的目的是给程序员们一点建议,告诉他们如何在每天最平凡的编程任务中帮助将来的阅读者理解代码的意图。

本书的深度应该介于Design Patterns(中译版《设计模式:可复用面向对象软件的基础》)和Java语言手册之间。Design Patterns讨论的是开发过程中每天要做几次的那种决策,通常是关于如何协调对象之间交互的决策,实现模式的粒度更小。编程时每过几秒钟就可能用上一个模式。语言手册很好地介绍了“能用Java做什么”,但对于“为什么使用某种结构”或者“别人读到这段代码时会怎么想”这样的问题谈论甚少,而这正是实现模式的重点所在。

在写这本书时,我的一个原则就是只写我熟悉的主题。比如说并发(concurrency)问题就没有涵盖在这些实现模式中,并非因为并发不重要,只是因为我对这个主题没有太多可说的。我对待并发问题的策略一向很简单:尽可能地把涉及并发的部分从我的程序中隔离出去。虽然我一直用这个办法还干得不错,但这确实没有多少可解释的。更多关于并发处理的实践指导,我推荐诸如Java Concurrency in Practice(中译版《Java并发编程实践》)之类的书籍。

本书完全没有涉及的另一个主题是软件过程。我给出的建议只是告诉阅读者如何用代码来交流,不管这代码是在一个漫长流程的最后阶段编写出来的,还是在编写出一个无法通过的测试之后立即编写出来的,我希望这些建议都同样适用。总而言之,不管冠以怎样的名目,只要能降低软件开发的成本就是好事。

此外本书也尽量避免使用Java的最新特性。我在选择技术时总是倾向于保守,因为无所不用其极地尝试新技术已经伤害过我太多次了(作为学习新技术的策略,这很好;但对于大部分开发工作而言,风险太大)。所以,你会在本书中看到一个非常朴实的Java子集。如果希望使用Java的最新特性,可以从别的地方去学习它们。

本书总共分成了7大块,如图1.1所示,分别是:

图1.1 全书概览

该言归正传了。如果你打算按部就班地读下去,请翻到下一页(我猜这用不着特别提醒)。如果想快速浏览所有的模式,请从第5章开始。


编程中的很多决策是无法复制的。开发网站的方式与开发心脏起搏器控制软件的方式肯定迥然不同。但决策的内容越接近纯技术化,其中的相似性就越多,我不是刚编写过一样的代码吗?程序员为不断重复的琐事耗费的时间越少,他们就有越多的时间来解决好真正独一无二的问题,从而更高效地编程。

绝大多数程序都遵循一组简单的法则。

模式就是基于这样的共性之上的。比如说,每个程序员都必须决定如何进行迭代遍历。在思考如何写出循环的时候,大部分领域问题都被暂时抛在脑后了,留下的就是纯技术问题:这个循环应该容易读懂,容易编写,容易验证,容易修改,而且高效。

让你操心的这一系列事情,就是模式的源起。上面列出的这些约束,或者叫压力(force),会影响程序中每个循环编写的方式。可以预见到,这样的压力会不断重现,这也正是模式之所以成为模式的原因:它其实是关于压力的模式。

有好几种合理的方式可以写出一个循环,它们分别暗含着对这些约束不同的优先级排序:如果性能更重要,你可能用这种方式来写循环;如果容易修改更重要,你就可能用另一种方式来写循环。

每个模式都代表着一种对压力进行相对优先级排序的观点。大部分模式都由一篇短文来描述,其中列举出解决某一问题的各种方案,以及推荐方案的优点所在。这些模式不仅给出一个建议,而且还讲出背后的原因,这样阅读者就可以自己判断应该如何解决这类重复出现的问题。

正如前面暗示的,每个模式也都带着一个解决方案的种子。关于“循环遍历一个容器”的模式可能会建议说“使用Java 5的for循环来描述遍历操作”。模式在抽象的原则和具体的实践之间架起了一座桥梁。模式可以帮助你编写代码。

模式彼此协作。建议你用for循环的模式,又引出了“如何给循环变量命名”的问题。我们不尝试把所有事情都塞进一个模式里,还有另一个模式专门讲“如何给变量命名”的话题。

模式在本书中有多种不同的展现形式:有时它们有清晰的名称,还有专门的章节来讨论压力和解决方案。但也有时,一些比较小的模式就直接放在更大的模式内部来介绍,一两句话或许就能够把一个小模式讨论清楚了。

使用模式有时会让你感到束手束脚,但确实可以帮你节省时间和精力。打个比方,就好像铺床这件小事,如果每次都必须思考每个步骤怎么做、找出正确的顺序,那就会比习惯成自然的做法耗费更多的精力。正是因为有一组铺床的模式,这件事情才得以大大简化。如果床恰好顶在墙边,或者床单太小,你会根据情况调整策略,但整体来说还是遵循固定模式来铺床,这样你的脑子就可以用来思考更有意思、更有必要的东西。编程也是一样,当模式成为习惯之后,我很开心地发现自己不必再为“如何写一个循环”而展开讨论了。如果整个团队都对一个模式不满,那么他们可以讨论引入新的模式。

没有任何一组模式能够适用于所有情况。本书中列出的模式是我在应用程序开发中亲自用过的,或者看到别人用过并且效果不错的(后文也浅谈了一下框架开发中的模式)。盲目效仿别人的风格,永远都不如思考和实践自己的风格并在团队中讨论交流来得有效。

模式最大的作用就是帮助人们做决定。有些实现模式最终会融入编程语言,就好像setjmp( )/longjmp( )结构变成了如今的异常处理。不过大部分时候,模式需要加以调整才能投入使用。

从这一章开始,我们试图寻找一种更节约、更快速、更省力的方式来解决常见的编程问题。使用模式可以帮助程序员用更合理的方式来解决常见问题,从而把更多的时间、精力和创造力留下来解决真正独一无二的问题。每个模式都涉及一个常见的编程问题,随后我们会讨论其中起影响作用的各种因素,并提出具体的建议:如何快速实现一个令人满意的解决方案。其结果是,这些模式将帮助读者更好、更快、更省力地完成编程工作中乏味的部分,从而留下更多的时间和精力来解决程序中独一无二的问题。

本书中的实现模式共同构筑了一种编程风格,下一章“一种编程理论”将会介绍这种编程风格背后的价值观和原则。


就算是再巨细靡遗的模式列表,也不可能涵盖编程中所遇到的每一种情况。你免不了(甚至常常)会遭遇到这种情景:上穷碧落,也找不到对应的现成解决方案。于是便需要有针对特定问题的通用解决方案。这也正是学习编程理论的原因之一。原因之二则是那种知晓如何去做、为何去做之后所带来的胸有成竹。当然,如果把编程的理论和实践结合起来讨论,内容就会更加精彩了。

每个模式都承载着一点点理论。但实际编程中存在一些更加深广的影响力,远不是孤立的模式所能概括的。本章将会讲述这些贯穿于编程中的横切概念,它们分为两类:价值观与原则。价值观是编程过程的统一支配性主题。珍视与其他人沟通的重要性,把代码中多余的复杂性去掉,并保持开放的心态,这才是我工作状态最佳的表现。这些价值观——沟通、简单和灵活——影响了我在编程时所做的每个决策。

此处描述的原则不像上面的价值观那样意义深远,不过每一项原则都在许多模式中得以体现。价值观有普遍的意义,但往往难以直接应用;模式虽可以直接应用,却是针对于特定情景;原则在价值观和模式之间搭建了桥梁。我早已发现,在那种没有模式可以应用,或是两个相互排斥的模式可以同等应用的场合,如果把编程原则弄清楚,对解决疑难会是一件好事。在面对不确定性的时候,对原则的理解让我可以“无中生有”创造出一些东西,同时能和其他的实践保持一致,而且结果一般都不错。

价值观、原则和模式,这3种元素互为补充,组成了一种稳定的开发方式。模式描述了要做什么,价值观提供了动机,原则把动机转化成了实际行动。

这里的价值观、原则和模式,是通过我的亲身实践、反思以及与其他人的讨论总结出来的。我们都曾经从前人那里吸收经验,最终会形成一种开发方式,但不是唯一的开发方式。不同的价值观和不同的原则会产生不同的方式。把编程方式用价值观、原则和模式的形式展现出来,其优点之一就是可以更加有效地展现编程方法的差异。如果你喜欢用某种方式来做事,而我喜欢另一种,那么就可以识别出我们在哪种层次上存在分歧,从而避免浪费时间。如果我们各自认可不同的原则,那么争论哪里该用大括号根本无助于解决问题。

有3个价值观与卓越的编程血脉相连,它们分别是:沟通、简单和灵活。虽然它们有时候会有所冲突,但更多的时候则是相得益彰。最优秀的程序会为未来的扩展留下充分的选择余地,不包含不相关的元素,容易阅读,容易理解。

如果阅读者可以理解某段代码,并且进一步修改或使用它,那么这段代码的沟通效果就很好。在编程时,我们很容易从计算机的角度进行思考。但只有一面编程一面考虑其他人的感受,我才能编写出好的代码。在这种前提下编写出的代码更加干净易读,更有效率,更清晰地展现出我的想法,给了我全新的视角,减轻了我的压力。我的一些社会性需要得到了自我满足。最开始编程吸引我的部分原因在于我可以通过编程与外界交流,然而,我不想与那些难缠又无法理喻的烦人家伙打交道。过了20年,把别人当作空气一样的编程方式才在我眼中褪尽了颜色。耗尽心神去精心搭建一座糖果城堡,于我而言已毫无意义。

Knuth所提出的文学编程理论促使我把注意力放到沟通上来:程序应该读起来像一本书一样。它需要有情节和韵律,句子间应该有优雅的小小跌宕起伏。

我和Ward Cunningham第一次接触到文学性程序这个概念以后,我们决定来试一试。我们找出Smalltalk中最干净的代码之一——ScrollController,坐到一起,然后试着把它写成一个故事。几个小时以后,我们以自己的方式完全重写了这段代码,把它变成了一篇合情合理的文章。每次遇到难以解释清楚的逻辑,重新把它写一遍都要比解释这段代码为何难以理解容易得多。沟通的需要改变了我们对于编码的看法。

在编程时注重沟通还有一个很明显的经济学基础。软件的绝大部分成本都是在第一次部署以后才产生的。从我自己修改代码的经验出发,我花在阅读既有代码上的时间要比编写全新的代码长得多。如果我想减少代码所带来的开销,我就应该让它容易读懂。

注重沟通还可以帮助我们改进思想,让它更加现实。一方面是由于投入更多的思考,考虑“如果别人看到这段代码会怎么想”所需要调动的脑细胞,和只关注自己是不一样的。这时我会退后一步,从全新的视角来审视面对的问题和解决方案。另一方面则是由于压力的减轻,因为我知道自己所做的事情是在务正业,我做的是对的。最后,作为社会性的产物,明确地考虑社会因素要比在假设它们不存在的情况下工作更为现实。

Visual Display of Quantitative Information一书中,Edward Tufte做过一个实验,他拿过一张图,把上面没有增加任何信息的标记全都擦掉,最终得到了一张很新颖的图,比原先那张更容易理解。

去掉多余的复杂性可以让那些阅读、使用和修改代码的人更容易理解。有些复杂性是内在的,它们准确地反映出所要解决的问题的复杂性。但有些复杂性的产生完全是因为我们忙着让程序运行起来,在摆弄过程中留下来的“指甲印”没擦干净。这种多余的复杂性降低了软件的价值,因为一方面软件正确运行的可能性降低了,另一方面将来也很难进行正确的改动。回顾自己做过的事情,把麦子和糠分开,是编程中不可或缺的一部分。

简单存在于旁观者的眼中。一个可以将专业工具使用得得心应手的高级程序员,他所认为的简单事情对一个初学者来说可能会比登天还难。只有把读者放在心里,你才可以写出动人的散文。同样,只有把读者放在心里,你才可以编写出优美的程序。给阅读者一点挑战没有关系,但过多的复杂性会让你失去他们。

在复杂与简单的波动中,计算机技术不断向前推进。直到微型计算机出现之前,大型机架构的发展倾向仍然是越来越复杂。微型计算机并没有解决大型机的所有问题,只不过在很多应用中,那些问题已经变得不再重要。编程语言也在复杂和简单的起伏中前行。C++在C的基础上产生,而后在C++的基础上又出现了Java,现在Java本身也变得越来越复杂了。

追求简单推动了进化。JUnit比它所大规模取代的上一代测试工具简单得多。JUnit催生了各种模仿者、扩展软件和新的编程/测试技术。它最近一个版本JUnit 4已经失去了那种“一目了然”的效果,虽然每一个导致其复杂化的决定都有我参与其中,但亦未能阻止这种趋势。总有一天,会有人发明一种比JUnit简单许多的方式,以方便编程人员编写测试。这种新的想法又会推动另一轮进化。

在各个层次上都应当要求简单。对代码进行调整,删除所有不提供信息的代码。设计中不出现无关元素。对需求提出质疑,找出最本质的概念。去掉多余的复杂性后,就好像有一束光照亮了余下的代码,你就有机会用全新的视角来处理它们。

沟通和简单通常都是不可分割的。多余的复杂性越少,系统就越容易理解;在沟通方面投入越多,就越容易发现应该被抛弃的复杂性。不过有时候我也会发现某种简化会使程序难以理解,这种情况下我会优先考虑沟通。这样的情形很少,但常常都表示这里应该有一些我尚未察觉的更大规模的简化。

在三种价值观中,灵活是衡量那些低效编码与设计实践的一把标尺。以获取一个常量为例,我曾经见到有人会用环境变量保存一个目录名,而那个目录下放着一个文件,文件中写着那个常量的值。为什么弄这么复杂?为了灵活。程序是应该灵活,但只有在发生变化的时候才需如此。如果这个常量永远不会变化,那么付出的代价就都白费了。

因为程序的绝大部分开销都是在它第一次部署以后才产生,所以程序必须要容易改动。想象中明天或许会用得上的灵活性,可能与真正修改代码时所需要的灵活性不是一回事。这就是简单性和大规模测试所带来的灵活性比专门设计出来的灵活性更为有效的原因。

要选择那些提倡灵活性并能够带来及时收益的模式。对于会立刻增加成本但收效却缓慢的模式,最好让自己多一点耐心,先把它们放回口袋里,需要的时候再拿出来。这样就可以用最恰当的方式使用它们。

灵活性的提高可能以复杂性的提高为代价。比如说,给用户提供一个可自定义配置的选择提高了灵活性,但是因为多了一个配置文件,编程时也需要考虑这一点,所以也就更复杂了。反过来简单也可以促进灵活。在前面的例子中,如果可以找到取消配置选项但又不丧失价值的方式,那么这个程序以后就更容易改动。

增进软件的沟通效果同样会提高灵活性。能够快速阅读、理解和修改你的代码的人越多,它将来发生变化的选择就越多。

本书中介绍的模式会通过帮助编程人员创建简单、可以理解、可以修改的应用程序来提高程序的灵活性。

实现模式并不是无缘无故产生的。每一种模式都或多或少体现了沟通、简单和灵活这些价值观。原则是另一个层次上的通用思想,比价值观更贴近于编程实际,同时又是模式的基础。

我们有很多理由来检查一下这些原则。正如元素周期表帮助人们发现了新的元素,清晰的原则也可以引出新的模式。原则可以解释模式背后的动机,它是有普遍意义的。在对立模式间进行选择时,最好的方式就是用原则来说话,而不是让模式争来争去。最后,如果遇到从未碰到过的情况,对原则的理解可以充当我们的向导。

例如,假如要使用新的编程语言,我可以根据自己对原则的理解发展出有效的编程方式,不必盲目模仿现有的编程方式,更不用拘泥于在其他语言中形成的习惯(虽然可以用任何语言编写FORTRAN风格的代码,但不该那么做)。对原则的充分理解使我能够快速地学习,即使在新鲜局面下仍然能够一以贯之地符合原则。接下来的部分,我将为你讲述隐藏在模式背后的原则。

组织代码结构时,要保证变化只会产生局部化影响。如果这里的一个变化会引出那里的一个问题,那么变化的代价就会急剧上升了。把影响范围缩到最小,代码就会有极佳的沟通效果。它可以被逐步深入理解,不必一开始就要鸟瞰全景。因为实现模式背后一条最主要的动机就是减少变化所引起的代价,所以局部化影响这条原则也是很多模式的形成缘由之一。

最小化重复这条原则有助于保证局部化影响。如果相同的代码出现在很多地方,那么改动其中一处副本时,就不得不考虑是否需要修改其他副本;变动不再只发生在局部。代码的复制越多,变化的代价就越大。

复制代码只是重复的一种形式。并行的类层次结构也是其一,同样破坏了局部化影响原则。如果修改一处概念需要修改两个或更多的类层次结构,就表示变化的影响已经扩散了。此时应重新组织代码,让变化只对局部产生影响。这种做法可以有效改进代码质量。

重复不容易被预见到,有时在出现以后一段时间才会被觉察。重复不是罪过,它只是增加了变化的开销。

我们可以把程序拆分成许多更小的部分——小段语句、小段方法、小型对象和小型包,从而消除重复。大段逻辑很容易与其他大段逻辑出现重复的代码片断,于是就有了模式诞生的可能,虽然不同的代码段落中存在差异,但也有很多相似之处。如果能够清晰地表述出哪些部分程序是等同的,哪些部分相似性很少,而哪些部分则截然不同,程序就会更容易阅读,修改的代价也会更小。

局部化影响的必然结果就是将逻辑与数据捆绑。把逻辑与逻辑所处理的数据放在一起,如果有可能尽量放到一个方法中,或者退一步,放到一个对象里面,最起码也要放到一个包下面。在发生变化时,逻辑和数据很可能会同时被改动。如果它们被放在一起,那么修改它们所造成的影响就会只停留在局部。

在编码开始的那一刻,我们往往不太清楚该把逻辑和数据放到哪里。我可能在A中编写代码的时候才意识到需要B中的数据。在代码正常工作之后,我才意识到它与数据离得太远。这时候我需要做出选择:是该把代码挪到数据那边去,还是把代码挪到逻辑这边来,或者把代码和数据都放到一个辅助对象中?也许还可能意识到,这时我还没法找出如何组合它们以便增进沟通的最好方式。

对称性也是我随时随地运用的一项原则。程序中处处充满了对称性。比如add()方法总会伴随着remove()方法,一组方法会接受同样的参数,一个对象中所有的字段都具有相同的生命周期。识别出对称性,把它清晰地表述出来,代码将更容易阅读。一旦阅读者理解了对称性所涵盖的某一半,他们就会很快地理解另外一半。

对称性往往用空间词汇进行表述:左右对称的、旋转的,等等。程序中的对称性指的是概念上的对称,而不是图形上的对称。代码中对称性的表现,是无论在什么地方,同样的概念都以同样的形式呈现。

这是一个缺少对称性的例子:

void process() {
  input();
  count++;
  output();
}

第二条语句比其他的语句更加具体。我会根据对称性的原则重写它,结果是:

void process() {
  input();
  incrementCount();
  output();
}

这个方法依然违反了对称性。这里的input()和output()操作都是通过方法意图来命名的,但是incrementCount()这个方法却以实现方式来命名。在追求对称性的时候,我会考虑为什么我会增加这个数值,于是就有了下面的结果:

void process() {
  input();
  tally();
  output();
}

在准备消灭重复之前,常常需要寻找并表示出代码中的对称性。如果在很多代码中都存在类似的想法,那么可以先把它们用对称的方式表示出来,让接下来的重构有一个良好开端。

实现模式背后的另一条原则是尽可能声明式地表达出意图。命令式的编程语言功能强大灵活,但是在阅读时需要跟随着代码的执行流程。我必须在大脑中建起一个程序状态、控制流和数据流的模型。对于那些只是陈述简单事实,不需要一系列条件语句的程序片断,如果用简单的声明方式写出来,读着就容易多了。

比如在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()方法更为灵活,但这应该是另外一本书里的故事了。)

最后一个原则就是把具有相同变化率的逻辑、数据放在一起,把具有不同变化率的逻辑、数据分离。变化率具有时间上的对称性。有时候可以将变化率原则应用于人为的变化。例如,如果开发一套税务软件,我会把计算通用税金的代码和计算某年特定税金的代码分离开。两类代码的变化率是不同的。在下一年中做调整的时候,我会希望能够确保上一年中的代码依然奏效。分离两类代码可以让我更确信每年的修改只会产生局部化影响。

变化率原则也适用于数据。一个对象中所有成员变量的变化率应该差不多是相同的。只会在一个方法的生命周期内修改的成员变量应该是局部变量。两个同时变化但又和其他成员的变化步调不一致的变量可能应该属于某个辅助对象。比如金融票据的数值与币种会同时变化,那么这两个字段最好放到一个辅助对象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这两个初始字段是对称的,它们会同时变化。但它们与对象中其他的字段是不对称的。把它们放到自己应该从属的对象中,让新的对象向阅读者传达出它们的对称关系,这样就更有可能在将来消除重复,进一步达到影响的局部化。

本章介绍了实现模式的理论基础。沟通、简单和灵活这三条价值观为模式提供了广泛的动机。局部化影响、最小化重复、将逻辑与数据捆绑、对称性、声明式表达和变化率这6条原则帮助我们将价值观转化为实际行动。接下来我们将会进入模式的世界,看一看针对编程实战中频繁出现的问题,会有哪些特定的解决方案。

注重通过代码与人沟通是一件有价值的事情,我们将在下一章“动机”中探寻其背后的经济因素。


相关图书

有限元基础与COMSOL案例分析
有限元基础与COMSOL案例分析
程序员的README
程序员的README
现代控制系统(第14版)
现代控制系统(第14版)
现代软件工程:如何高效构建软件
现代软件工程:如何高效构建软件
GitLab CI/CD 从入门到实战
GitLab CI/CD 从入门到实战
科学知识图谱:工具、方法与应用
科学知识图谱:工具、方法与应用

相关文章

相关课程