实现模式(修订版)(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-15 19:29:51

点击下载

作者:[美]KentBeck著

出版社:信息技术第一出版分社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

实现模式(修订版)

实现模式(修订版)试读:

前言

这是一本关于编程的书,更具体一点,是关于“如何编写别人能懂的代码”的书。编写出别人能读懂的代码没有任何神奇之处,这就与任何其他形式的写作一样:了解你的阅读者,在脑子里构想一个清晰的整体结构,让每个细节为故事的整体作出贡献。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,感谢你们。第1章引言

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

这本书的目标是要帮助你通过代码表达自己的意图。首先,我们对编程和模式做一个概述(第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 的最新特性,可以从别的地方去学习它们。1.1 章节概览

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

总体介绍(Introduction)——这几个简短的章节描述了“用代码沟通”的重要性与价值所在,以及实现模式背后的思想;

类(Class)——这部分的模式讲述了为什么要创建类,如何创建类,如何用类来书写逻辑等问题;

状态(State)——关于状态存取的模式;

行为(Behavior)——这部分的模式告诉阅读者如何用代码来表现逻辑,特别是如何用多种不同的方式来做这件事;

方法(Methods)——关于如何编写方法的模式,它们将告诉你,根据你对方法的分解和命名,阅读者会作出怎样的判断;

容器(Collections)——关于选择和使用容器的模式;

改进框架(Frameworks)——上述模式的变体,适用于框架开发(而非应用程序开发)。图1.1 全书概览1.2 那么,现在……

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

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

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

更多的时候,程序是在被阅读,而不是被编写。

没有“完工”一说。修改程序的投入会远大于最初编写程序的投入。

程序都由一组基本的语句和控制流概念组合而成。

程序的阅读者需要理解程序——既从细节上,也从概念上。有时他们从细节开始,逐渐理解概念;有时他们从概念开始,逐渐理解细节。

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

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

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

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

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

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

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

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

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

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

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

本书中的实现模式共同构筑了一种编程风格,下一章“一种编程理论”将会介绍这种编程风格背后的价值观和原则。第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 灵活

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

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

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

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

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

本书中介绍的模式会通过帮助编程人员创建简单、可以理解、可以修改的应用程序来提高程序的灵活性。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这两个初始字段是对称的,它们会同时变化。但它们与对象中其他的字段是不对称的。把它们放到自己应该从属的对象中,让新的对象向阅读者传达出它们的对称关系,这样就更有可能在将来消除重复,进一步达到影响的局部化。3.3 小结

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

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

30年前,Yourdon和Constantine在Structured Design一书中将经济学作为了软件设计的底层驱动力。软件设计应该致力于减少整体成totaldevelop本。软件成本cost可以被分解为初始成本cost和维护成本maintaincost:totaldevelopmaintain

cost= cost+ cost

当这个行业在软件开发的过程中慢慢积累了经验以后,人们发现,软件的维护成本要远远高于它的初始成本。这个结果让大多数人都倒吸了一口冷气。(那些对维护的需求很小或者根本不需要维护的项目,它们所使用的模式应该和本书所讲述的实现模式迥异。)

维护的代价很大,这是因为理解现有代码需要花费时间,而且容易出错。知道了需要修改什么以后,做出改动就变得轻而易举了。掌握现在的代码做了哪些事情是最需要花费人力物力的部分。改动之后,还要进行测试和部署。maintainunderstandchangetestdeploy

cost= cost+ cost+ cost+ cost

减少整体成本的策略之一是在初期的开发中投入更多精力,希望借此减少甚至消除维护的需要。这些做法往往会失败。一旦代码以未预期的方式发生变化,人们所曾做出的任何预见都不再是万全之策。人们可能会为了防备将来发生的变化而过早考虑代码的通用性,但如果出现了没有预料到而又势在必行的变化,先前的做法往往就会与现实发生冲突。

从本质上看,增加软件的先期投入是与两条重要的经济学原则——金钱的时间价值和未来的不确定性——相悖的。今天的一元钱会比明天的一元钱更值钱,所以从原则上讲,我们的实现策略应该是尽量将支出推后。同样,由于不确定性的存在,实现策略应该更倾向于带来即时收益而非长远收益。这听上去好像在鼓励人们目光短浅一些,不去考虑将来,但实际上这些实现模式一方面着眼于获得即时收益,另一方面也在创建干净的代码,以方便将来的开发工作。

我用来减少整体成本的策略是,要求所有开发人员在进行维护的时候注重程序员与程序员之间的沟通,减少理解代码所带来的代价。清晰明确的代码会带来即时收益:代码缺陷更少,更易共享,开发曲线更加平滑。

将一些实现模式形成习惯后,我的开发速度得到了提升,令我分心的想法也更少了。刚开始写下最初的几个实现模式的时候(The Smalltalk Best Practice Patterns,Prentice Hall,1996),我自以为是个编程能手。为了促使自己把注意力放在模式上,我在记录下所遵守的模式之前一个字符的代码也不肯输入。那段经历实在是很折磨人,就像把手指扭结在一起打字一样。在第一个星期内,每编写一分钟的程序都要先进行几个小时的写作。到了第二个星期,我发现大多数的基本模式都已经就绪了,大部分时间我只是在遵守这些现成的模式编程而已。还没到第三个星期,我就比从前的开发速度快了很多,因为我已经认认真真地检查过自己的开发方式,不会再有各种疑惑在我大脑中反复唠叨干扰思路了。

这些实现模式只有一部分是我自己的发明。我的开发方式有很大一部分都是从早一代程序员那里借鉴过来的。这些良好的编程习惯存在于那些容易阅读、容易理解并容易维护的代码之中,将它们落为明文以后,我的编码速度得到了提升,也变得更加流畅。在为将来做好准备的同时,我还可以更快地完成今天的代码。

在编写本书的过程中,我既总结了个人的编程习惯,也从已有的代码中寻找灵感。我读过 JDK、Eclipse 和我以往开发经历中的代码,并将它们进行了比较。最后所形成的这些模式,是想帮助读者清晰地认识到该如何编写人们可以理解的代码。关注的方向不同,价值观念不同,就会形成不同的模式。比如在“改进框架”一章中,我撰写了专门适合开发框架的实现模式。开发框架时的价值取向不同于一般开发,所以其实现模式也不同于一般的实现模式。

就像为经济目的服务一样,实现模式也在为人类服务。代码来自于人,服务于人。编程人员可以使用实现模式来满足人本身的需要,比如从工作中获得成就感,或者成为社区中为人信任的一员。在后续的章节中,我会继续讨论模式给人和经济两方面带来的影响。第5章类

类的概念早在柏拉图之前就出现了。比如说,5种柏拉图立体 [1]就是5个类,它们的实例随处可见。柏拉图立体是绝对完美的,但它们并不实际存在。至于我们身边那些触手可及的实例,它们总有某些不甚完美的方面。

面向对象编程像柏拉图之后的西方哲学家一样延续了这种思维。面向对象编程把程序划分成许多类,类是对一组相似的东西的一般归纳,而对象则是这些东西本身。

类对于沟通很重要,因为它们可以描述很多具体的东西。实现模式最大的跨度只到类一级;与之相比,设计模式则主要是在讨论类与类之间的关系。

本章将会介绍下列模式:

类(Class)——用一个类来表示“这些数据应该放在一起,还有这些逻辑应该也和它们在一起”;

简单的超类名(Simple Superclass Name)——位于继承体系根上的类应该有简单的名字,用以描绘它的隐喻;

限定性的子类名(Qualified Subclass Name)——子类的名字应该表达出它与超类之间的相似性和差异性;

抽象接口(Abstract Interface)——将接口与实现分离;

interface——用Java的interface机制来表现不常变化的抽象接口;

有版本的interface(Versioned Interface)——引入新的子interface,从而安全地对interface进行扩展;

抽象类(Abstract Class)——用抽象类来表现很可能变化的抽象接口;

值对象(Value Object)——这种对象的行为就好像数值一样;

特化(Specialization)——清晰地描述相关计算之间的相似性和差异性;

子类(Subclass)——用一个子类表现一个维度上的变化;

实现器(Implementor)——覆盖一个方法,从而表现一种计算上的变化;

内部类(Inner Class)——将只在局部有用的代码放在一个私有的类中;

实例特有的行为(Instance-specific Behavior)——每个实例的逻辑都有不同;

条件(Conditional)——明确指定条件,以表现不同的逻辑;

委派(Delegation)——把操作委派给不同类型的对象,以表现不同的逻辑;

可插拔的选择器(Pluggable Selector)——通过反射来调用方法,以表现不同的逻辑;

匿名内部类(Anonymous Inner Class)——在方法内部创建一个新对象,并覆盖其中的一两种方法,以表现不同的逻辑;

库类(Library Class)——如果一组功能不适合放进任何对象,就将其描述为一组静态方法。5.1 类

数据的变化比逻辑要频繁得多,正是这种现象让类有了存在的意义。每个类其实就是这样一个声明:这些逻辑应该放在一起,它们的变化不像它们所操作的数据那么频繁;这些数据也应该放在一起,它们变化的频率差不多,并且由与之关联的逻辑来负责处理。这种“数据会变、逻辑不变”的划分并非绝对适用:有时随着数据值的不同,逻辑也会有所不同;有时逻辑也会发生相当大的变化;有时数据本身在计算的过程中反倒不会改变。学会如何用类来包装逻辑和如何表达逻辑的变化,这是有效使用对象编程的重要部分。

把多个类放进一个继承体系可以缩减代码量,比原封不动地把超类的内容照抄到所有子类精简得多。和所有缩减代码量的技巧一样,它也让代码变得更难读懂;必须理解超类的上下文,然后才有可能理解子类。

正确使用继承也是有效使用对象编程的一方面。子类传递的信息应该是:我和超类很像,只有少许差异。(我们经常说在“子类”中“覆盖”一种方法,这听起来难道不奇怪吗?要是当初精心挑选一个好的隐喻,程序员的日子应该好过得多吧。)

在由对象搭建而成的程序中,类是相对昂贵的设计元素。一个类应该做一些有直接而明显的意义的事情。减少类的数量是对系统的改进,只要剩下的类不因此而变得臃肿就好。

后面的模式介绍了如何通过类的声明来表达设计思路。5.2 简单的超类名

找到一个贴切的名字是编程中最令人开心的时刻之一。你一直为一个含糊不清的念头而困扰,代码变得越来越复杂,但你总觉得它可以不必那么复杂。然后,往往是在闲聊时,有人冒了一句:“噢,我明白了,不就是个调度器(Scheduler)吗。”于是大家向后一靠,长舒一口气。贴切的名字能引发连锁反应,带来更深入的简化与改进。

在所有的命名当中,类的命名是最重要的。类是维系其他概念的核心。一旦类有了名字,其中操作的名字也就顺理成章了。相反的情况却很少成立,除非类的名字一开始命名得太糟糕。

类名的“简短”与“表现力”之间存在张力。你会在交谈中用到类名:“记得在平移 Figure 之前先要旋转一下吗?所以类名应该简明扼要,但有时候一个类名又要用到好几个单词才足够精确。

摆脱这种两难境地的办法就是给计算逻辑找到强有力的隐喻。脑子里有了隐喻,一个个单词就不只是单词,而是一张张关系、连接和暗示的大网。比如说在开发HotDraw这个绘图框架时,我一开始把图画(drawing)中的对象命名为DrawingObject。Ward Cunningham带来了一个印刷方面的隐喻:一幅图画就好像印刷出来、排版妥当的纸页,纸页上画出来的元素正是图形(figure),于是这个类的名字就变成了Figure。有了这个隐喻作为铺垫,Figure这个名字不仅比原来的DrawingObject更简短,而且更准确、更具表现力。

有时候需要花些时间才能想出一个好名字。甚至可能代码已经“完工”,投入运行了好几周、好几个月甚至(我真的遇到过这种情况)好几年之后,突然想到了一个更好的类名。有时候需要强迫自己找到一个好名字,抽出一本辞典,写下所有多少有些接近的名字,站起来走一走。另一些时候应该带着挫败感和对时间的信心先去考虑新功能的实现,潜意识会默默起作用的。

交谈总能帮助我想出更好的名字。要尝试把一个对象的用途解释给别人听,我就得寻找具有表现力和感染力的图景来描述它,这样的图景往往能引出新的名字。

对于重要的类,尽量用一个单词来为它命名。5.3 限定性的子类名

子类的名字有两重职责,不仅要描述这些类像什么,还要说明它们之间的区别是什么。同样,在这里需要权衡长度与表现力。与位于继承体系根上的超类不同,子类的名字在交谈中用得并不频繁,所以值得以牺牲简明来换取更好的表现力。通常在超类名的基础上扩展一两个词就可以得到子类名。

这条规则也有例外:如果继承只是用作共享实现的机制,并且子类本身就代表一个重要的概念,那么这样的子类就应该被视为它自己的继承体系的根,拥有一个简单的名字。举例来说,HotDraw里有一个Handle类,代表当图形被选中时对其进行编辑操作。尽管它继承自Figure类,它还是有一个简单的名字:Handle。在它之下还有一大堆的子类,它们的名字也大多扩展自Handle,例如StretchyHandle、TransparencyHandle等。由于Handle是这个继承体系的根,因此它更应该取一个简单的超类名,而不是加上各种修饰语扩展而成的子类名。

给多级继承体系中的子类命名也是一个难题。一般而言,多级继承体系应该进行重构,换成使用委派,但既然它们还在这里,就应该给它们一个好名字。不要不假思索地在直接超类的基础上扩展出子类名,要多从阅读者的角度来想想阅读者需要了解这个类的什么信息。你应该带着这个思考,以超类名为参考来给子类命名。

与他人沟通是类名的用途,如果仅仅为了和计算机沟通,只要给每个类编号就足够了。太长的类名读写都费劲,太短的类名又会考验阅读者的记忆力。如果一组类的名字体现不出它们之间的相关性,阅读者就很难对它们形成整体印象,也很难回忆起它们的关系。应该用类名来讲述代码的故事。5.4 抽象接口

请牢记软件开发的古训:针对接口编程,不要针对实现编程。从另一个角度来说,这也意味着设计决策不应该暴露给不必要的地方。如果大部分代码只知道我在处理一个容器,那么我就可以随时改变这个容器的具体实现。但有时不得不指定具体类,否则计算就没法进行下去。

这里所说的“接口”是指“一组没有实现的操作”。在 Java 中,接口这个概念既可以表现为 interface,也可以表现为超类。随后的两个模式会分别指出两者的适用场景。

每层接口都有成本:需要学习它,理解它,给它写文档,调试它,组织它,浏览它,还有给它命名。并不是接口数量越多软件成本就会越少,只有需要接口带来的灵活性时才值得为它付出成本。所以,既然很多时候并不能提前知道是否需要接口带来的灵活性,出于降低成本的考虑,在仔细考虑“哪些地方需要接口”的同时,最好是在真正需要这种灵活性时再引入接口。

尽管我们成天都在抱怨软件不够灵活,但很多时候系统根本不需要变得更灵活。不管是要进行基础性的修改(例如改变整数类型的字节数)还是大范围的修改(例如引入新的商业模型),大部分软件都不会需要最大限度的那种灵活性。

在引入接口时的另一个经济方面的考量是软件的不可预测性。我们这个行业似乎已经沉溺于这样一种观念:只要一开始设计正确,软件系统就不需要任何变动。最近我读到了一份关于“软件变更的理由”的列表,其中列举的条目包括程序员没有弄清需求、客户改变了想法,等等,唯一没有提到的是正当的变更。这样的一份列表传达出的信息是:变更总是错误的。可是,为什么一份天气预报不能永远正确呢?因为天气以不可预测的方式变化。同样的道理,为什么我们不能一次列出系统中所有需要灵活性的地方呢?因为需求和技术都在以不可预测的方式变化。这并非要给我们程序员免责,我们仍然要尽全力开发客户当下需要的系统;但它让我们知道,通过预先思考来弄清软件将来的样子,其效果是相当有限的。

所有这些因素——对灵活性的需要、灵活性的成本、“何处需要灵活性”的不可预测——加在一起让我相信:应该在确定无疑地需要灵活性时,才应该引入这种灵活性。引入灵活性是有代价的,因为需要修改已有的软件。如果不能独自完成所有需要的修改,成本就会更高,我们在后面关于“改进框架”的章节中会详细讨论这个话题。

Java 有两种方式来表现抽象接口:超类和 interface。它们在应对变化时涉及的成本各有不同。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载