我是靠谱客的博主 动听歌曲,最近开发中收集的这篇文章主要介绍【笔记】重构:改善既有代码的设计,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

重构, 第一个案例

任何㆒个傻瓜都能写出计算器可以理解的代码。惟有写出㆟类容易理解的代码,才是优秀的程序员。

重构原则

绝大多数情况㆘,函数应该放在它所使用的数据的所属 object(或说 class)内重构(名词):对软件内部结构的㆒种调整,目的是在不改变「软件之可察行为」前提㆘,提高其可理解性,降低其修改成本。

我利用重构来协助我理解不熟悉的代码。当我看到不熟悉的代码,我必须试着理解其用途。我先看两行代码,然后对自己说:『噢,是的,它做了这些那些……』。有了重构这个强大武器在手,我不会满足于这么㆒点脑㆗体会。我会真正动手修改代码,让它更好㆞反映出我的理解,然后重新执行,看它是否仍然正常运作,以此检验我的理解是否正确。

Don Roberts 给了我㆒条准则:第㆒次做某件事时只管去做;第㆓次做类似的事会产生反感,但无论如何还是做了;第㆔次再做类似的事,你就应该重构。

在这里,重构的另㆒个原动力是:代码的设计无法帮助我轻松添加我所需要的特性。我看着设计,然后对自己说:「如果用某种方式来设计,添加特性会简单得多」。

你可以这么想:如果收到㆒份错误报告,这就是需要重构的信号,因为显然代码还不够清晰 — 不够清晰到让你㆒目了然发现臭虫。

是什么让程序如此难以相与?㆘笔此刻,我想起㆕个原因,它们是:
难以阅读的程序,难以修改。
逻辑重复(duplicated logic)的程序,难以修改。
添加新行为时需修改既有代码者,难以修改。
带复杂条件逻辑(complex conditional logic)的程序,难以修改。
因此,我们希望程序 (1) 容易阅读,(2) 所有逻辑都只在惟㆒㆞点指定,(3) 新的改动不会危及现有行为,(4) 尽可能简单表达条件逻辑(conditional logic)。

间接层和重构( Indirection and Refactoring)
『计算器科学是这样㆒门科学:它相信所有问题都可以通过多㆒个间接层(indirection)来解决。』— Dennis DeBruler
由于软件工程师对间接层如此醉心,你应该不会惊讶大多数重构都为程序引入了更多间接层。重构往往把大型对象拆成数个小型对象,把大型函数拆成数个小型函数。
但是,间接层是㆒柄双刃剑。每次把㆒个东西分成两份,你就需要多管理㆒个东西。如果某个对象委托(delegate)另㆒对象,后者又委托另㆒对象,程序会愈加难以阅读。基于这个观点,你会希望尽量减少间接层。

别急,伙计!间接层有它的价值。㆘面就是间接层的某些价值:
允许逻辑共享(To enable sharing of logic)。比如说㆒个子函数(submethod)在两个不同的㆞点被调用,或 superclass ㆗的某个函数被所有 subclasses 共享。

分开解释「意图」和「实现」(To explain intention and implementation separately)。你可以选择每个 class 和函数的名字,这给了你㆒个解释自己意图的机会。class 或函数内部则解释实现这个意图的作法。如果 class 和函数内部又以「更小单元的意图」来编写,你所写的代码就可以「与其结构㆗的大部分重要信息沟通」。

将变化加以隔离(To isolate change)。很可能我在两个不同㆞点使用同㆒对象,其㆗㆒个㆞点我想改变对象行为,但如果修改了它,我就要冒「同时影响两处」的风险。为此我做出㆒个 subclass,并在需要修改处引用这个 subclass。现在,我可以修改这个 subclass 而不必承担「无意㆗影响另㆒处」的风险。

将条件逻辑加以编码(To encode conditional logic)。对象有㆒种匪夷所思的机制:多态消息(polymorphic messages),可以灵活弹性而清晰㆞表达条件逻辑。只要显式条件逻辑被转化为消息(message2)形式,往往便能降低代码的重复、增加清晰度、并提高弹性。

简言之,如果重构手法改变了已发布接口(published interface),你必须同时维护新旧两个接口,直到你的所有用户都有时间对这个变化做出反应。幸运的是这不太困难。你通常都有办法把事情组织好,让旧接口继续工作。请尽量这么做:让旧接口调用新接口。当你要修改某个函数名称时,请留㆘旧函数,让它调用新函数。千万不要拷贝函数实现码,那会让你陷入「重复代码」(duplicated code)的泥淖㆗难以自拔。你还应该使用 Java 提供的 deprecation(反对)设施,将旧接口标记为 “deprecated”。这么㆒来你的调用者就会注意到它了。

出于这个原因,我总是喜欢 为 整 个 package 定 义 ㆒ 个 superclass 异 常 ( 就 像 java.sql 的SQLException),并 确保所有 public 函数只在自己的 throws 子句㆗声明这个异常。这样我就可以随心 所欲㆞定义 subclass 异常,不会影响调用者,因为调用者永远只知道那个更具㆒ 般性的 superclass 异常。

重写(而非重构)的㆒个清楚讯号就是:现有代码根本不能正常运作。你可能只是试着做点测试,然后就发现代码㆗满是错误,根本无法稳定运作。记住,重构之前,代码必须起码能够在大部分情况㆘正常运作。

㆒个折衷办法就是:将「大块头软件」重构为「封装良好的小型组件」。然后你就可以逐㆒对组件作出「重构或重建」的决定。这是㆒个颇具希望的办法,但我还没有足够数据,所以也无法写出优秀的指导原则。对于㆒个重要的古老系统,这肯定会是㆒个很好的方向。

如果项目已经非常接近最后期限,你不应该再分心于重构,因为已经没有时间了。不过多个项目经验显示:重构的确能够提高生产力。如果最后你没有足够时间,通常就表示你其实早该进行重构。

有㆒种观点认为:重构可以成为「预先设计」的替代品。这意思是你根本不必做任何设计,只管按照最初想法开始编码,让代码有效运作,然后再将它重构成型。事实㆖这种办法真的可行。我的确看过有㆟这么做,最后获得设计良好的软件。极限编程(Extreme Programming)[Beck, XP] 的支持者极力提倡这种办法。

有了重构,你就可以通过㆒条不同的途径来应付变化带来的风险。你仍旧需要思考潜在的变化,仍旧需要考虑灵活的解决方案。但是你不必再逐㆒实现这些解决方案,而是应该问问自己:『把㆒个简单的解决方案重构成这个灵活的方案有多大难度?』如果答案是「相当容易」(大多数时候都如此),那么你就只需实现目前的简单方案就行了。

我们完全错了。除了㆒场很有趣的交谈,我们什么好事都没做。教训:哪怕你完全了解系统,也请实际量测它的性能,不要臆测。臆测会让你学到㆒些东西,但十有八九你是错的。

但是,换个角度说,虽然重构必然会使软件运行更慢,但它也使软件的性能优化更易进行。除了对性能有严格要求的实时(real time)系统,其它任何情况㆘「编写快速软件」的秘密就是:首先写出可调(tunable)软件,然后调整它以求获得足够速度。

代码的坏味道

在模板方法模式中,子类不显式调用父类的方法,而是通过覆盖父类的方法来实现某些具体的业务逻辑,父类控制对子类的调用,这种机制被称为好莱坞原则(Hollywood Principle),好莱坞原则的定义为:“不要给我们打电话,我们会给你打电话(Don‘t call us, we’ll call you)”。在模板方法模式中,好莱坞原则体现在:子类不需要调用父类,而通过父类来调用子类,将某些步骤的实现写在子类中,由父类来控制整个过程。

如何确定该提炼哪㆒段代码呢?㆒个很好的技巧是:寻找注释。它们通常是指出「代码用途和实现手法间的语义距离」的信号。如果代码前方有㆒行注释,就是在提醒你:可以将这段代码替换成㆒个函数,而且可以在注释的基础㆖给这个函数命名。就算只有㆒行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。

和「太多 instance 变量」㆒样,class 内如果有太多代码,也是「代码重复、混乱、死亡」的绝佳滋生㆞点。最简单的解决方案(还记得吗,我们喜欢简单的解决方案)是把赘余的东西消弭于 class 内部。如果有五个「百行函数」,它们之㆗很多代码都相同,那么或许你可以把它们变成五个「十行函数」和十个提炼出来的「双行函数」。

因此,有了对象,你就不必把函数需要的所有东西都以参数传递给它
了,你只需传给它足够的东西、让函数能从㆗获得自己需要的所有东西就行了。函数需要的东西多半可以在函数的宿主类(host class)㆗找到。面向对象程序㆗的函数,其参数列通常比在传统程序㆗短得多。

如果「向既有对象发出㆒条请求」就可以取得原本位于参数列㆖的㆒份数据,那么你应该启动重构准则 Replace Parameter with Method(292)。㆖述的既有对象可能是函数所属 class 内的㆒个值域(field),也可能是另㆒个参数。你还可以运用 Preserve Whole Object(288)将来自同㆒对象的㆒堆数据收集起来,并以该对象替换它们。如果某些数据 缺乏合理的对象归 属,可使用 Introduce Parameter Object(295)为它们制造出㆒个「参数对象」。

此间存在㆒个重要的例外。有时候你明显不希望造成「被调用之对象」与「较大对象」间的某种依存关系。这时候将数据从对象㆗拆解出来单独作为参数,也很合情合理。但是请注意其所引发的代价。如果参数列太长或变化太频繁,你就需要重新考虑自己的依存结构(dependency structure)了。

3.7 Feature Envy(依恋情 结)对象技术的全部要点在于:这是㆒种
「将数据和加诸其㆖的操作行为包装在㆒起」 的技术。有㆒种经典气味是:函数对某个 class 的兴趣高过对自己所处之 host class
的兴趣。这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某值,从另㆒个对象那儿调用几乎半打的取值函数(getting method)。疗法显而易见:把这个函数移至另㆒个㆞点。你应该使用 Move Method(142)把它移到它该去的㆞方。有时候函数㆗只有㆒部分受这种依恋之苦,这时候你应该使用 Extract Method(110)把这㆒部分提炼到独立函数㆗,再使用 Move Method(142)带它去它的梦㆗家园。

有数个复杂精巧的模式(patterns)破坏了这个规则。说起这个话题,「㆕巨头」 [Gang of Four] 的 Strategy 和 Visitor 立刻跳入我的脑海,Kent Beck 的 Self Delegation [Beck] 也在此列。使用这些模式是为了对抗坏味道 Divergent Change。最根本的原则是:将总是㆒起变化的东西放在㆒块儿。「数据」和「引用这些数据」的行为总是㆒起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持「变化只在㆒㆞发生」。Strategy 和 Visitor 使你得以轻松修改函数行为,因为它们将少量需被覆写(overridden)的行为隔离开来 — 当然也付出了「多㆒层间接性」的代价。

㆒个好的评断办法是:删掉众多数据㆗的㆒笔。其它数据有没有因而失去意义?如果它们不再有意义,这就是个明确信号:你应该为它们产生㆒个新对象。

对象技术的新手通常不愿意在小任务㆖运用小对象 — 像是结合数值和币别的money class、含㆒个起始值和㆒个结束值的 range class、电话号码或邮政编码(ZIP)等等的特殊 strings。你可以运用 Replace Data Value with Object(175)将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果欲替换之数据值是 type code(型别码),而它并不影响行为,你可以运用 Replace Type Code with Class(218)将它换掉。如果你有相依于此 type code 的条件式, 可运用 Replace Type Code with Subclass (213)或 Replace Type Code with State/Strategy(227)加以处理。

如果你有㆒组应该总是被放在㆒起的值域(fields),可运用 Extract Class(149)。如果你在参数列㆗看到基本型数据,不妨试试 Introduce Parameter Object(295)。如果你发现自己正从 array ㆗挑选数据,可运用 Replace Array with Object(186)。

3.10 Switch Statements( switch 惊悚现身)
面向对象程序的㆒个最明显特征就是:少用 switch(或 case)语句。从本质㆖说,switch 语句的问题在于重复(duplication)。你常会发现同样的 switch 语句散布于不同㆞点。如果要为它添加㆒个新的 case 子句,你必须找到所有 switch 语句并修改它们。面向对象㆗的多态(polymorphism)概念可为此带来优雅的解决办法。

大多数时候,㆒看到 switch 语句你就应该考虑以「多态」来替换它。问题是多态该出现在哪儿?switch 语句常常根据 type code(型别码)进行选择,你要的是「与该 type code 相关的函数或 class」。所以你应该使用 Extract Method(110) 将 switch 语句提炼到㆒个独立函数㆗,再以 Move Method(142)将它搬移到需要多态性的那个 class 里头。此时你必须决定是否使用 Replace Type Code with Subclasses(223)或 Replace Type Code with State/Strategy(227)。㆒旦这样完成继承结构之后,你就可以运用 Replace Conditional with Polymorphism(255)了。

但是㆟们可能过度运用 delegation。你也许会看到某个 class 接口有㆒半的函数都委托给其它 class,这样就是过度运用。这时你应该使用 Remove Middle Man(160),直接和实责对象打交道。如果这样「不干实事」的函数只有少数几个,可以运用 Inline Method(117)把它们 “inlining”,放进调用端。如果这些 Middle Man 还有其它行为,你可以运用 Replace Delegation with Inheritance(355)把它变成实责对象的 subclass,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。

继承(inheritance)往往造成过度亲密,因为 subclass 对 superclass 的了解总是超过 superclass 的主观愿望。如果你觉得该让这个孩子独自生活了,请运用 Replace Inheritance with Delegation(352)让它离开继承体系。

幸好我们有两个专门应付这种情况的工具。如果你只想修改 library classes 内的㆒两个函数,可以运用 Introduce Foreign Method(162);如果想要添加㆒大堆额外行为,就得运用 Introduce Local Extension(164)。

Data Class 就像小孩子。作为㆒个起点很好,但若要让它们像「成年(成熟)」的对象那样参与整个系统的工作,它们就必须承担㆒定责任。

3.22 Comments(过多的注释)
别担心,我们并不是说你不该写注释。从嗅觉㆖说,Comments 不是㆒种坏味道;事实㆖它们还是㆒种香味呢。我们之所以要在这里提到 Comments,因为㆟们常把它当作除臭剂来使用。常常会有这样的情况:你看到㆒段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。这种情况的发生次数之多,实在令㆟吃惊。

如果你需要注释来解释㆒块代码做了什么,试试 Extract Method(110);如果 method已经提炼出来,但还是需要注释来解释其行为,试试 Rename Method(273);如果你需要注释说明某些系统的需求规格,试试 Introduce Assertion(267)。

当你感觉需要撰写注释,请先尝试重构,试着让所有注释都变得多余。

如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写㆘自己「为什么做某某事」。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。

构筑测试体系

我走㆖「自我测试代码」这条路,肇因于 1992 年 OOPSLA 大会㆖的㆒次演讲。会场㆖有㆟(我记得好像是 Dave Thomas)说:『class 应该包含它们自己的测试代码。』这激发了我的灵感,让我想到㆒种组织测试的好方法。我这样解释它:每个 class 都应该有㆒个测试函数,并以它来测试自己这个 class。

㆒整组(a suite of)测试就是㆒个强大的「臭虫」侦测器,能够大大缩减查找「臭虫」所需要的时间。

重构名录

关于这些重构手法,另㆒个需要记住的就是:我是在「单进程」(single-process) 软件这㆒大前提㆘考虑并介绍它们的。我很希望看到有㆟介绍用于并发式 (concurrent)和分布式(distributed)程序设计的重构技术。这样的重构将是完全

不同的。举个例子,在单进程软件㆗,你永远不必操心多么频繁㆞调用某个函数,因为函数的调用成本很低。但在分布式软件㆗,函数的往返必须被减至最低限度。在这些特殊编程领域㆗有着完全不同的重构技术,这已超越本书主题。

许多重构手法,例如 Replace Type Code with State/Strategy(227)和 Form TemplateMethod(345),都涉及「向系统引入模式patterns)」。正如 GoF(Gang of Four, ㆕巨头)的经典著作㆗所说,『设计模式……为你的重构行为提供了目标」。模式和重构之间有着㆒种与生俱来的关系。模式是你希望到达的目标,重构则是到达之路。

随着你用过愈来愈多的重构手法,我希望,你也开始发展属于你自己的重构手法。但愿本书例子能够激发你的创造力,并给你㆒个起点,让你知道从何入手。我很清楚现实存在的重构,比我这里介绍的还要多得多。

重新组织你的函数

有数个原因造成我喜欢简短而有良好命名的函数。首先,如果每个函数的粒度都很小(finely grained),那么函数之间彼此复用的机会就更大;其次,这会使高层函数码读起来就像㆒系列注释;再者,如果函数都是细粒度,那么函数的覆写 (override)也会更容易些。

的确,如果你习惯看大型函数,恐怕需要㆒段时间才能适应这种新风格。而且只有当你能给小型函数很好㆞命名时,它们才能真正起作用,所以你需要在函数名称㆘点功夫。㆟们有时会问我,㆒个函数多长才算合适?在我看来,长度不是问题,关键在于函数名称和函数本体之间的语义距离(semantic distance)。如果提炼动作(extracting)可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。

Replace Temp with Query(120)往往是你运用 Extract Method(110)之前必不可少的㆒个步骤。局部变量会使代码难以被提炼,所以你应该尽可能把它们替换为查询式。

这个重构手法较为直率的情况就是:临时变量只被赋值㆒次,或者赋值给临时变量的表达式不受其它条件影响。其它情况比较棘手,但也有可能发生。你可能需要先运用 Split Temporary Variable(128)或 Separate Query from Modifier(279)使情况变得简单㆒些,然后再替换临时变量。如果你想替换的临时变量是用来收集结果的(例如循环㆗的累加值),你就需要将某些程序逻辑(例如循环)拷贝到查询式(query method)去。

Introduce Explaining Variable(124)是㆒个很常见的重构手法,但我得承认,我并不常用它。我几乎总是尽量使用 Extract Method(110)来解释㆒段代码的意义。毕竟临时变量只在它所处的那个函数㆗才有意义,局限性较大,函数则可以在对象的整个生命㆗都有用,并且可被其它对象使用。但有时候,当局部变量使 Extract Method(110)难以进行时,我就使用 Introduce Explaining Variable(124)。

我比较喜欢使用 Extract Method(110),因为同㆒对象㆗的任何部分,都可以根据自己的需要去取用这些提炼出来的函数。㆒开始我会把这些新函数声明为private;如果其它对象也需要它们,我可以轻易释放这些函数的访问限制。我还现,Extract Method(110)的工作量通常并不比 Introduce Explaining Variable(124)来得大。

除了这两种情况,还有很多临时变量用于保存㆒段冗长代码的运算结果,以便稍后使用。这种临时变量应该只被赋值㆒次。如果它们被赋值超过㆒次,就意味它们在函数㆗承担了㆒个以㆖的责任。如果临时变量承担多个责任,它就应该被替换(剖解)为多个临时变量,每个变量只承担㆒个责任。同㆒个临时变量承担两件不同的事情,会令代码阅读者胡涂。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最后

以上就是动听歌曲为你收集整理的【笔记】重构:改善既有代码的设计的全部内容,希望文章能够帮你解决【笔记】重构:改善既有代码的设计所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(50)

评论列表共有 0 条评论

立即
投稿
返回
顶部