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

重构,绝对是写程序过程中最重要的事之一。在写程序之前不可能事先了解所有的需求,设计肯定会有考虑不周的地方,而且随着项目需求的修改,也有可能原来的设计已经被改的面目全非了。更何况,很少有机会从到到尾完成一个项目,基本上都是接手别人的代码,即使这个项目从头到尾参与,也有可能接手其它组员的代码。有这样的经验,看到别人的代码时感觉就像屎一样,有一种强烈的想重写的冲动,但一定要压制住这种冲动,完全重写,可能比原来好一点,但浪费时间不说,还有可能引入原来不存在的bug,而且,你不一定比原来设计的好,也许原来的设计考虑到了一些你没考虑到的情况。我们写的代码,终有一天也会被别人接手,很有可能到时别人会有和我们现在一样的冲动。所以,我们要做的重构,从小范围的重构开始。

重构不只是可以改善既有的设计,还可以帮助我们理解原来很难理解的流程。比如一个复杂的条件表达式,可能需要很久才能明白这个表达式的作用,这时候,抽象出来,起一个易于理解的名字,函数名字很重要,下次再见到的时候,自然知道当初的想法了,好的代码胜过注释,毕竟注释有可能更新的不是很及时。

《重构,改善既有代码的设计》,这是一本经典之作,看过这本书要收获的是,让重构融入整个写代码的过程中,让重构不再作为一项独立的任务,而是在写代码的过程中随时随地的进行,一个函数不容易理解,重构;添加新功能时很不方便,重构。

重构、改善既有代码的设计插图

1、定义

对软件内部结构的一种调整,使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构,提高其可理解性,降低其修改成本。

2、为何重构

  • 重构改进软件设计:原来的设计不可能考虑到所有的情况,随意添加功能修改东西,可能已经看不出原本的设计了
  • 重构使软件更容易理解
  • 重构帮助找到bug:重构可以增加对代码的理解,从而容易发现bug
  • 重构提高编程速度:重构虽然花费时间,但是重构可以改善程序的设计,使程序更不容易出现bug,使添加新特性更容易

3、何时重构

无须专门拨出时间进行重构,重构应该随时随地进行,事不过三,三则重构,当添加新功能时如果不是特别容易,可以通过重构使添加新特性更容易,修补错误时重构可以更容易发现bug,复审代码也是重构的好时机

4、代码的坏味道

  • 重复代码
  • 过长函数
  • 过大的类
  • 过长参数列
  • 发散式变化:一个类受多种变化的影响
  • 散弹式修改:一种变化引发多个类响应修改
  • 依恋情结:函数对某个类的兴趣高过对自己所处类的兴趣,是时候考虑这个函数到底应该放在什么位置了
  • 数据泥团:两个类中相同的字段,许多函数中相同的参数,这时候就可以让他们拥有自己的类了,简而言之,类似的东西写一个类里
  • 基本类型偏执:编写小对象,如表示范围的range
  • switch惊悚现身:switch带来重复,同样的switch语句经常散布于不同的地址,如果要加一个新的case子句,就必须找到所有switch语句并修改他们
  • 平行继承体系:每当你为某个类增加一个子类,必须也为另一个类增加一个子类,大多数时候你会发现,某个继承体系的类名前缀和另一个继承体系的类名前缀完全相同,是时候分离为两个继承体系了
  • 冗赘类:没啥用的类就应该干掉
  • 夸夸其谈的未来性:无用的抽象类,无用的预留参数
  • 令人迷惑的局部变量
  • 过度耦合的消息链:函数过大时,就应该提取函数
  • 中间人:无用的委托,过多中间层
  • 异曲同工的类:不同的类或函数,干相同的事,写一个不好吗
  • 过多的注释:如果一个函数需要过多的注释,是时候重构了,把代码当注释,好的名字就是注释

5、重构组织函数

  • 提炼函数:代码粒度越小越容易重用,而且这部分代码被组织到一起后,可以起一个易于理解的名字解释其意图。要注意临时变量的处理
  • 内联函数:对于一些函数,可能其实现和名字一样容易理解,没必要再封装一层;或者一些没必要的中间层都可以去掉,java中用final修饰,C++中用inline修饰,c#中没有内联函数,也许微软认为C++才需要关心性能,而C#关注快速开发,不必理会这些开销吧。
  • 内联临时变量:你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其它重构手法
  • 以查询取代临时变量:应减少定义临时变量,如程序以一个临时变量保存某一表达式的运算结果并作为方法的返回值,直接用表达式作为返回结果就算了
  • 引入解释性变量:有时候也会遇到非常复杂的表达式,定义一个临时变量,解释其用途也是可以的
  • 分解临时变量
  • 移除对参数的赋值:不要对函数传进来的参数赋值
  • 建立新的类取代函数:有一个大型函数,这个函数有太多的临时变量,以至于不能重构,这时就可以另起炉灶,新建一个类,原来函数的参数就变成了新的类成员,可以方便的对新的勒种响应的函数进行各种重构。
  • 替换算法:将两个方法中相同的部分,提取出来

6、在对象之间搬移特性

  • 搬移函数:类中的一个函数使用另一个类的对象的次数比使用自己所在类的对象的次数还要多,很有可能这个函数定义错地方了
  • 搬移字段
  • 提炼类
  • 内联类
  • 隐藏委托关系:有时调用一个对象的方法时获得另一个对象,然后再调用获得对象的一个方法获得另一对象,如此反复,此时可以直接调用第一个对象再加一个方法直接返回最终对象
  • 移除中间人

7、重新组织数据

  • 自封装数据:为字段建立赋值取值函数
  • 以对象取代数据值:有一个数据项,可能需要对这个数据项添加一个行为,把这个数据项封装为一个类
  • 将值对象改为引用对象
  • 将引用对象改为值对象:对象不好控制时改为不可变的值对象,这样就不用了考虑同步的问题了。
  • 以对象取代数组:一个数组中的不同元素表示不同的东西,不容易理解
  • 复制“被监视数据”:主要针对GUI中的数据,实现UI与逻辑分享,MVC,将V中的数据复制到M中,并在M中数据变化时通过listener进制或observer弄死同步更新UI
  • 封装字段:public改为private,提供相应的访问函数

8、简化条件表达式

  • 分解条件表达式:有一个复杂的条件表达式,将if条件,then、else中的三个段落全部提取出独立函数以方便理解
  • 合并条件表达式
  • 合并重复的条件判断:在条件表达式的每个分支上有着相同的一段代码,把这段代码搬移到条件表达式之外
  • 移除控制标记:不用通过控制标记来决定是否退出循环或跳过函数剩下的操作,直接break或return
  • 以卫语句取代嵌套表达式:如果某个条件不常见,应该单独检查该条件,这种操作成为卫语句
  • 以多态取代条件表达式
  • 引入null对象
  • 引入断言

9、简化函数调用

  • 函数改名:好的函数名字很重要,名字起得好可以看出函数具体是做什么的,对于理解复杂逻辑非常有帮助
  • 添加参数
  • 移除参数
  • 将查询函数和修改函数分离
  • 令函数携带参数
  • 以明确函数取代参数:有一个函数,其中完全取决于参数值而采取不同的行为,针对该参数的每一个可能性,建立一个单独的函数
  • 引入参数对象:某些参数总是同时出现,先建一个变量取代这些参数,减少参数的数量
  • 移除赋值函数:如果类中的某个应该在对象创建时被赋值,此后不再改变,不要添加赋值函数
  • 隐藏函数:有一个函数从来没有被其它类用到,或者本来被用到,但随着类添加接口,之后就用不到了,那么隐藏这个函数,也就是减小作用域
  • 封装向下转型:如果返回的值一定需要调用者转型,那么最好在函数中完成转型动作
  • 以异常取代错误码
  • 以测试取代异常:异常只应该被用于异常的、罕见的、意料之外的行为,不应该作为条件检查用

10、处理概括关系

  • 字段上移:连个子类拥有相同的字段,将该字段移至父类消除重复
  • 函数上移:有些函数在各个子类中产生相同的结果,上移至父类消除重复并方便修改
  • 构造函数本体上移
  • 函数下移
  • 字段下移
  • 提炼子类
  • 提炼父类:两个类有相似的特性,为这两个类建立一个父类,将相同特性移至父类
  • 体连接口
  • 折叠继承体系:父类与子类无太大区别,之前没考虑明白,消除继承关系,合并在一起
  • 塑造模板函数
  • 以委托取代继承:某个子类只使用父类接口中的一部分,将父类作为子类的一个字段,消除继承关系
  • 以继承取代委托:一个类的行为基本上都是委托另一个类,当另一个类接口改变时也要同时修改委托类,直接继承省事方便

11、大型重构

  • 梳理并分解继承体系:某个继承体系同时承担两项责任,建立两个继承体系,并通过委托关系让其中一个调用另一个
  • 将过程化设计转化为对象设计
  • 将领域和表述/显示分离
  • 提炼继承关系:某个类做了太多的事情,其中一部分工作是以大量条件表达式完成的,建立继承体系,以一个子类表示一种特殊情况

发表评论