走出重构(Refactoring)的误区

一、什么是重构?


重构(Refactoring)这个词最初由Martin Fowler 和 Kent Beck给下的定义,它是

一种修改,使软件的内部结构更容易理解,在不改变软件的可见行为方式前提下使软件更容易变更…它是一种有节制的整理代码、使bug产生几率最小化的方法。


二、重构不是重新设计


有些人喜欢把对一个系统的重新设计或重写或重新搭建平台或返工叫“大规模重构”。因为技术上讲,这些并不改变软件功能特征——业务逻辑、软件输入和输出仍和以前一样,“只是”设计和代码实现变了。它和常规重构的区别看起来就是:一个是重写了一段代码,一个是重写了一个系统,只要你是一步一步做下来的,你都可以称之为“重构”——不管你是长年累月被困于将一个老系统换成新代码,还是对系统架构进行大规模的改造。

  “大规模重构”会变的很糟糕。你可能需要花数周、数月(甚至数年)才能完成,需要你对软件的很多部分进行改动。软件会因此不能运行,需要分多次发布这些变更,需要你做临时的台架(scaffolding)和变通方案——尤其是你采用短周期的敏捷开发方法时。这时Branch by Abstraction这样的实践方法就派上用场了,它能帮你在长周期内管理代码中的变化。

  而且在开发新代码的同时你还要维护旧代码,这使得代码版本控制很麻烦,变更起来不方便,致使代码很脆弱,易犯错——这正和重构所预期的目的背道而驰。有时这样的情况会一直持续下去——这种新旧代码交替的过程永远不能完成,因为能获得最大利益的部分都是最先完成,或者因为最初带来这个想法的顾问已经干别的去了,或者是预算被消减,而且你也讨厌维护这样一个拖拉的项目。

          在一些重型的项目开发过程中混入重构的概念是不对的。它们从根本上就是另外一种工作,带有完全不同的开发成本和风险。它混淆了人们对什么是重构、重构能干什么的认识。

  重构可以、也应该融入到你写代码或维护代码的过程中——作为日常开发/质量管理的组成部分,就像写测试和代码审查一样。重构应该被安静的,持续的和低调的完成。它需要我们把工作精力分出一部分给它,它需要在我们的工期评估和风险评估中考虑到它的存在。如果做的正确,你不需要去解释或向外人验证这部分工作。

  花几分钟、一两个小时做重构,就像是你开发过程中的一种修改,是工作的一部分。如果它让你花了数天时间,或者更长,那不是重构;那是重写,或重新设计。如果你需要明确的留出一部分时间(或整个sprint周期)来重构代码,如果需要为清理代码而申请批准,或把清理代码作为一个开发需求,那你不是在重构——即使你用了重构的技术和工具,你仍然做的是另外一种工作。

  有些程序员认为对代码进行根本的、重大的修改是他们的权利和义务,在重构的名义下进行重新设计、重写,为了将来,也不辜负自己的技艺。重新设计和重写有时候是你正确的该做的事情。但出于坦诚和表述清楚,请不要把这些活动赋以重构的名义。


三、不要盲目重构


盲目主要体现在:

  1、在还没有对系统整体架构有个清晰认识的时候,就想用自认为新的技术或架构来替换。

  2、根本不分析现有系统架构或程序存在的弊端,只是一味地谈设计模式,以设计模式中固有的一套来重构(在重构中,它只作为一个参考,而不是一个依据。)

  3、重构比较随性,每个版本的开发都跳出架构之外随意带入新的设计思想

 

  这种盲目重构后给系统会带来更多问题:

  你会发现当你重构完后你的系统运行效率变低了,

  系统中同时存在多种思想,新加入人员更难接手,

  由于你没有完全了解系统,反而在你的重构当中带来了很多重复代码,

  最悲剧的是你重构后的代码也被其他人当成垃圾,而进行重构。


四、何时使用重构


Kent Beck提出了"代码坏味道"的说法,以下列述的代码症状:

  ·代码中存在重复的代码

  中国有118 家整车生产企业,数量几乎等于美、日、欧所有汽车厂家数之和,但是全国的年产量却不及一个外国大汽车公司的产量。重复建设只会导致效率的低效和资源的浪费。

  程序代码更是不能搞重复建设,如果同一个类中有相同的代码块,请把它提炼成类的一个独立方法,如果不同类中具有相同的代码,请把它提炼成一个新类,永远不要重复代码。

  过大的类和过长的方法

  过大的类往往是类抽象不合理的结果,类抽象不合理将降低了代码的复用率。方法是类王国中的诸侯国,诸侯国太大势必动摇中央集权。过长的方法由于包含的逻辑过于复杂,错误机率将直线上升,而可读性则直线下降,类的健壮性很容易被打破。当看到一个过长的方法时,需要想办法将其划分为多个小方法,以便于分而治之。

  牵一毛而需要动全身的修改

  当你发现修改一个小功能,或增加一个小功能时,就引发一次代码地震,也许是你的设计抽象度不够理想,功能代码太过分散所引起的。

  类之间需要过多的通讯

  A类需要调用B类的过多方法访问B的内部数据,在关系上这两个类显得有点狎昵,可能这两个类本应该在一起,而不应该分家。

  过度耦合的信息链

  "计算机是这样一门科学,它相信可以通过添加一个中间层解决任何问题",所以往往中间层会被过多地追加到程序中。如果你在代码中看到需要获取一个信息,需要一个类的方法调用另一个类的方法,层层挂接,就象输油管一样节节相连。这往往是因为衔接层太多造成的,需要查看就否有可移除的中间层,或是否可以提供更直接的调用方法。

  各立山头干革命

  如果你发现有两个类或两个方法虽然命名不同但却拥有相似或相同的功能,你会发现往往是因为开发团队成员协调不够造成的。笔者曾经写了一个颇好用的字符串处理类,但因为没有及时通告团队其他人员,后来发现项目中居然有三个字符串处理类。革命资源是珍贵的,我们不应各立山头干革命。

  不完美的设计

  在笔者刚完成的一个比对报警项目中,曾安排阿朱开发报警模块,即通过Socket向指定的短信平台、语音平台及客户端报警器插件发送报警报文信息,阿朱出色地完成了这项任务。后来用户又提出了实时比对的需求,即要求第三方系统以报文形式向比对报警系统发送请求,比对报警系统接收并响应这个请求。这又需要用到Socket报文通讯,由于原来的设计没有将报文通讯模块独立出来,所以无法复用阿朱开发的代码。后来我及时调整了这个设计,新增了一个报文收发模块,使系统所有的对外通讯都复用这个模块,系统的整体设计也显得更加合理。

  每个系统都或多或少存在不完美的设计,刚开始可能注意不到,到后来才会慢慢凸显出来,此时唯有勇于更改才是最好的出路。

  缺少必要的注释

  虽然许多软件工程的书籍常提醒程序员需要防止过多注释,但这个担心好象并没有什么必要。往往程序员更感兴趣的是功能实现而非代码注释,因为前者更能带来成就感,所以代码注释往往不是过多而是过少,过于简单。人的记忆曲线下降的坡度是陡得吓人的,当过了一段时间后再回头补注释时,很容易发生"提笔忘字,愈言且止"的情形。

  曾在网上看到过微软的代码注释,其详尽程度让人叹为观止,也从中体悟到了微软成功的一个经验。

五、保持兼容性


    重构的目的在于改变系统的实现方式,而不改变原有的功能。这个过程中,判断兼容性就十分的重要。一个子系统、模块、类、函数是否与升级前保持兼容,如何判断这个兼容性,如何保持这个兼容性,这关系到重构的成本和重构的可能性。

    程序员学习写程序代码时,会发现随着程序代码愈来愈多,许多的程序代码不断重复出现和被使用,因此很自然的开始使用例程、子程序或是过程、函数等机制帮助我们进行程序代码整理的工作。于是很自然的,字体的分析方式演化成这个样子:将客户的需求过程进行分解,一步一步的分解,直到可以直接的实现他。这就是面向过程的分析方式。

    面向过程的分析方式对变化的能力是很弱的。为什么呢?因为面向过程的分析方式很容易造成一种倾向——不区分行动的主体。一个过程是没有主体的,他不是在为自己工作,而是在为“别人”工作。当我们修改了一个过程之后,我们很难判断这个过程是否保持向后兼容,其他过程会不会受到影响。因为这个过程对外界有意义的不仅是他的输入和输出,还包括每一步过程,每一步过程都可能含有一个非常隐讳的业务意义,对外界产生影响。

    因此,修改一个过程是非常困难的,通常升级一个面向过程的系统,可以采用两种方式:
1、写新的过程;
2、在原有的过程上加开关参数。

    除此以外的升级办法都很难保证原过程和新过程的兼容性,容易造成错误。

    为了更好的保证升级后模块的兼容性,应该采用面向对象的分析方式。按照这样的分析方式,一个对象为“自己”工作,他有完整的、独立的业务含义。对象之间通过接口发生联系,一个对象对外界有影响的部分只有接口,至于他做什么、如何做、做的对不对,则不是外界需要关心的事情。

    判断一个接口升级后是否保持兼容性就是一件比较容易的事情了。我们可以判断接口的输入输出是否符合下面两条规则:
1、升级后的输入是升级前的输入的超级;
2、升级后的输出是升级前的输出的子集。

    只要符合这两点,他就仍然可以在系统中运行,不会对其他对象造成危害。在实际的工程中,判断这个兼容性有一个更好的办法:自动化的单元测试

    在重构的过程中,自动化的单元测试是非常好的保障。采用自动化的单元测试,不断运行测试,可以保证系统的结构改变的过程中,业务行为不发生改变。


下面是一些比较有深度的阅读材料。

  • 重构:改善既有代码的设计
  • 代码整洁之道
  • Working Effectively with Legacy Code
  • 程序员的职业素养

你可能感兴趣的:(走出重构(Refactoring)的误区)