重构-改善既有代码的设计

  Erich 在《重构》的序言中说到:代码被阅读和被修改的次数远远多于他被编写的次数。保持代码的易读、易修改的关键,就是重构。

重构的目的是什么?书中的第二章有专门的一节作了解释。不过第一章举了个简单案例,展现了一个相对完整的重构过程。这个案例初始一共只有三个类,其中一个类中的一个方法长达五十行左右,一眼望去,就两个字:不爽。就像书中说的它做的事情实在是太多了即便如此,这个程序还是能正常工作,……编译器才不会在乎代码好不好看呢,但是当我们打算修改系统的时候,就涉及到了人,而人在乎这些。差劲的系统是很难修改的,因为很难找到修改点。如果很难找到修改点,程序员就很有可能犯错,从而引入bug我觉得这就是为什么要重构的一个重要原因。

一个方法如果过长,说明里面做了很多事情,涉及到了很多步骤。这样的话一旦要对功能进行修改,就首先要读懂长长的一大段代码,然后再分解其中的功能,找到此次修改所涉及到的代码区等等一系列步骤,这样会给进一步开发带来很大的麻烦。而且,分解长的方法,可以帮助我们发现重复代码。重复代码的副作用恐怕大部分的编程人员都了解。就在今天,我在重构最近一个月开发的代码的时候,首先把一些好几十行的方法都拆分成小的代码段,拆分之后,我发现有两个类三个方法中,都执行了这样一段相同的逻辑:根据某个表的一个唯一索引查找一条记录。这样的代码完全可以拆分出来,以减少重复代码,如果以后库表结构发生变化,我们还可以只修改一处,否则,真的是改起来太困难了。而如果没有把长代码拆分的话,很难从那么多冗长的方法中,发现重复代码。

针对第一章简单案例,重构的大致过程如下:

我们首先拆分长函数,第一个步骤是找出代码的logical clump,并运用Extract Method。

另外,要注意使用能清晰表达业务逻辑含义的变量名。好的代码应该清楚表达出自己的功能,变量的名称是代码清晰的关键。所以更改变量名称是值得的行为。我就碰到过这样的代码:某个类提供一个对外的public方法,该方法主要功能是根据传入的一些参数,去数据库表中查找一些信息并返回。在这个方法的参数列表中,参数名使用了需要查询的库表的字段名。我们数据库表的字段命名规则是PK/FK+缩写表名+缩写实际字段名,在保证字段名称不能过长的原则下,这一路“缩”下来,如果不是对库表非常熟悉的人,仅仅通过字段名,很难判断它的业务含义。我们姑且不论数据库表的字段命名规则是否有问题,但以这种方式作为参数列表的参数名,让别人如何看懂?

在一种情况下我们应该怀疑一个方法是否放错了类:绝大多数情况下,函数应该放在他所使用的数据的所属Object(或者说class内),如果某个方法没有使用任何所属object的信息,而是全部使用另一个object的信息,我们应该考虑一下是否把这个方法移动一下

下一步,使用了Replace Temp with Query,即 将只是获取某个返回值,但并未再作任何改变的临时变量替换成了QueryMartin说这样做的原因是他们会导致大量参数被传来传去,而其实完全没有这个必要。你很容易失去他们的踪迹,尤其在长长的函数之中更是如此。但这样,我觉得势必会以牺牲性能为代价,Martin也提到了性能的问题,他曾在第一章两个地方谈到“重构与性能”的问题。但他认为,重构时不必担心性能,优化时才需要担心他们,但那个时候你已处于一个比较有利的位置,有更多的选择可以完成有效优化。但可能我还没有在实际工作中考虑到或者说碰到“重构与性能”这个问题。所以这个问题有待进一步思考。

在这个简单案例中,原本有一段逻辑是使用switch来根据一笔租片(Movie)的类别来计算它的费用。在这里,它在另一个对象的属性基础上运用switch语句,不是一个好主意,如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用

再往后的重构,是使用设计模式中的状态模式(State pattern)完成条件到多态的转换。由于对这个模式还不是很了解,这段内容暂且略过

   重构技术提供了一种更高效且受控的代码整理技术。

    重构的目的是使软件更容易被理解和修改。你可以在软件内部做很多修改,但必须对软件[可受观察之外部行为]制造成很小的变化。或甚至不造成变化。与之形成对比的是[性能优化]。和重构一样,性能优化通常不会改变组件的行为(除了执行速度),只会改变其内部结构。但是两者出发点不同:性能优化往往使代码较难理解,但为了得到所需的性能你不得不那么做。

    使用重构技术开发软件时,你把自己的时间非赔给两种截然不同的行为:添加新功能和重构。添加新功能时,你不应该修改既有代码,只管添加新功能。重构是你就不能再添加功能,只管改进程序的结构。你可能会在软件开发过程中,这两件事经常交替做,比如添加新功能时,发现程序结构改一下,功能添加会容易很多,于是进行重构,调整好后,又继续之前添加新功能的工作。无论何时,你应该清楚自己到底在做哪件事。

    为何重构:

     改进软件设计

     使软件更易被理解

     助你找到Bug

     祝你提高编程速度

         Martin有一段文字对重复代码对优秀设计的影响作了讨论,我觉得说得很透彻:

         设计不良的代码往往需要更多的代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事情。因此贵今设计的一个重要的方向就是消除重复代码(Duplicate Code)。代码量减少并不会使系统运行更快,因为这对程序的运行轨迹几乎没有任何明显影响。然而代码数量减少将使未来可能的程序修改动作容易得多。代码越多,正确的修改就越困难。因为有更多代码需要理解。你在这儿做了点修改,系统却不如预期那样工作,因为你未修改另一处——那的代码作者几乎完全一样的事情,只是所处环境略有不同。如果消除重复代码,你就可以确定代码将所有事物和行为都只表述一次,唯一一次,这正是优秀设计的根本。

    一开始的重构可能只停留在细枝末节上。随着代码渐趋简洁,会发现自己可以看到一些以前看不到的设计层面的东西。如果不对代码作这些修改,也许我永远看不见它们,因为我的聪明才智不足以在脑子里把这一切都想象出来。

    何时重构:重构应该随时随地进行。你之所以重构,是因为你想做别的什么事,而重构可以帮助你把那些事做好。

       三次法则:Three Strikes and you refactor

       添加功能时一并重构:使需要修改的代码更容易理解、代码的设计无法帮助我轻松添加我所需要的特性

       修补错误时一并重构:如果收到一份错误报告,这就是需要重构的信号,因为显然代码还不够清晰——不够清晰到让你一目了然发现bug(但我觉得有了错误报告,就是重构的信号有点过了)

       复审代码时一并重构:和某个团队进行设计复审,而和一个复审者进行代码复审。极限编程中的成对编程形式,把代码复审的积极性发挥到了极致。一旦采用这种形式,所有正式的开发任务都由两名开发者在同一台机器上进行。这样便在开发过程中形成随时进行的代码复审工作,而重构也就被包含在开发过程内了

    什么样的程序难以修改:难以阅读的、逻辑重复的、添加新行为时需要修改既有代码的、带复杂条件逻辑的

       因此我们希望程序:容易阅读、所有逻辑都只在唯一地点指定、新的改动不会危及现有行为、尽可能简单表达条件逻辑

    间接层(Indirection)和重构:

       间接层的价值:

         允许逻辑共享:比如一个子函数在两个不同的地点被调用,或superclass中的某个函数被所有subclasses共享

         分开解释意图和实现

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

         将条件逻辑加以编码:对象有一种匪夷所思的机制——多态消息(polymorphic messages),可以灵活弹性而清晰地表达条件逻辑。只要显示条件逻辑被转化为消息(message)形式,往往便能降低代码的重复、增加清晰度并提高弹性(注:发送消息给某个对象,即调用某个函数)

    如果重构手法改变了已发布接口,你必须同时维护新旧两个接口,直到你的所有用户都由这个时间对这个变化做出反应。这个时候应该让旧接口调用新接口。千万不要拷贝函数实现码,这会让你陷入重复代码的泥沼中难以自拔。你还应该是使用Java提供的deprecation

       Java之中还有一个特别关于修改接口的问题。在throws字句中增加一个异常,这并不是对

Signature的修改,所以你无法以delegation来隐藏它。由于这个原因,我总是喜欢为整个package定一个superclass异常(就象java.sqlSQLException),并确保所有public函数侄子自己的throws子句中声明这个异常,这样就可以解决上述的这个问题。

    在设计阶段应该问问自己:把一个简单的解决方案重构成这个灵活的方案有多大难度?如果答案是[相当容易],那么你就只需要实现目前的简单方案就行了

    当发现系统性能很差的时候:哪怕你完全了解你的程序,也请实际量测它的性能,不要臆测,臆测会让你学到一些东西,但十有八九你是错的

    重构与性能:重构往往会使软件运行更慢,但它也使软件的性能优化更易进行。除了对性能有严格要求的实时系统,其他任何情况下便携快速软件的秘密就是:首先写出可调软件,然后调整它以求获得足够速度

       三种编写快速软件的方法:

           时间预算法:通常只用于性能要求极高的实时系统。如果使用这种方法,分解你的设计时就要做好预算,给每个组件预先分配一定的资源——包括时间和执行轨迹,每个组件都不能超过自己的预算

           持续关切法:这种方法要求任何程序员在任何时间做任何事时,都要设法保持系统的高性能。这种方法很常见,感觉上很有吸引力,但通常不会起太大作用。因为性能改善一旦被分散到程序各角落,每次改善都只不过是从对程序行为的一个狭隘视角出发而已。如果对大多数程序进行分析,你会发现它把大半时间都耗费在一小半代码上,90%的优化工作都是白费劲儿,因为被你优化的代码有许多很难被执行起来。

           利用上述90%统计数据:采用这种方法时,你以一种良好的分解方式来建造自己的程序,不对性能投以任何关注,直至进入性能优化阶段——那通常在开发后期。一旦进入该阶段,你再按照某个特定程序来调整程序性能。在性能优化阶段,你首先应该以一个量测工具监控程序的运行,让他告诉你程序中哪些地方大量消耗时间和空间。这样你就可以找出性能热点所在的一小段代码。然后你应该集中关切这些性能热点,并使用前述持续关切法中的优化手段来优化它们。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值