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

前言

  • 任何一个傻瓜都可以写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员
重构的意图
重构不产生新的功能,狭义范围来说也不修复原有的bug
  • 重构:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本
    • 改进软件设计
    • 使软件更容易理解
    • 帮助找到bug
    • 提高编程速度
重构的时机
  • 过早的断言可扩展性容易造成过度设计
  • 在前期设计合理的前提下,增加新特性或修复bug时才是最合适重构的契机
  • 此时你能更好的理解什么地方是变化的,什么地方是设计不合理的
第一步:重构的前提
人非圣贤,孰能无过。我们无法保证重构不会引入bug,所以只能保证引入可靠的测试。
  • 第一步:保证对即将修改的代码提供一个可靠的测试环境
    • 越是庞大的软件,花时间去编写单元测试,在重构是会带来约巨大的便利
    • 确保所有测试是完全自动化,让他们检查自己的测试结果
    • 一套好的测试能大大缩减查找bug所需要的时间
第二步:找出代码的怀味道
如果尿布臭了,就换掉它
基本问题:重复、冗余
  • 重复代码
    • 范例:代码重复1次令人难受,重复2次就该考虑重构。
    • 问题:重复,复用差
    • 解决:提出方法/类:增强复用; 模板模式:增强复用
  • 过长函数
    • 范例:略
    • 问题:可读性差,逻辑混乱,可维护性差,可扩展性差
    • 解决:
      • 每当需要注释说明代码块功能时,就该提出方法了
      • 消除临时变量
      • 引入参数对象
      • 引入方法代理
      • 复杂的if-else Switch 提出处理方法
  • 过大的类
    • 范例:略
    • 问题:可读性差,逻辑混乱,可维护性差,可扩展性差
    • 解决:提出类/子类/代理类
  • switch、if-else 泛滥
    • 范例:散落各处的switch,到处都是switch
    • 问题:重复、可维护性差
    • 问题:最大隐患是case被多个switch使用,如此一来修改一个case需要寻找所有相关的switch。
    • 解决:尽量用多态代替多个Switch等判断,而且switch尽量提出到工厂方法这样的最前的判断中。状态模式、策略模式、工厂模式。
  • 临时字段
    • 范例:只被用一次,又不需要它代替注释的临时字段、临时变量
    • 问题:冗余
    • 解决:inline
  • 过度耦合的消息链
    • 范例:过长的引用链或调用链。有中间商(Adapter)赚差价不可怕,可怕的是你穿衣服要别人帮忙
    • 问题:耦合度过高
    • 解决:Move method,重新设计分层;EventBus(不得已之选)
  • 异曲同工的类
    • 范例:两个类实际在做类似的事情
    • 问题:冗余
    • 解决:合并
  • 过多的注释
    • 范例:略
    • 问题:注释只在迫不得已的时候用
    • 解决:方法名、变量名、常量名 代替注释
  • 冗余类
    • 范例:直接可以inline的多余类
    • 问题:冗余。其实和基本类型偏执是矛盾的,但凡事过犹未及
    • 解决:删除
领域、边界
  • 过长参数列
    • 范例:略
    • 问题:可读性差,参数关联性不被重视
    • 引入参数对象
  • 数据泥团
    • 范例:类似过长参数列,主要是有相关性的数据不被归类整理,甚至存在多个副本,被散落到多个类去处理
    • 问题:可读性差,参数关联性不被重视
    • 解决:引入参数对象。最简单的例子 Size。
  • 发散式变化
    • 范例:一个变化导致了多个方法甚至多个类都需要修改
    • 问题:可维护性差、领域边界问题
    • 解决:软件一但需要修改,我们希望只在一点进行修改。如果做不到这点,这代码的味道就相当刺鼻。最简单的例子,提出常量
  • 散弹式修改。
    • 范例:这是上一点的升级版,如果需要修改的代码散布四处,你不但很难找到她们,也很容易忘记某个重要的修改。(对于我们的代码来说,甚至散落到不同应用中)
    • 问题:可维护性差、领域边界问题
    • 解决:类似上一点
  • 依恋情节
    • 范例:一个类的方法对另一个类的属性的依赖高过自己本身,如为了计算某值经常从另一个对象那调用了n次
    • 问题:设计问题
    • 解决:Move method
  • 基本类型偏执
    • 范例:滥用基本数据类型,极少提出类;魔数到处是,常量到处放
    • 问题:数据关联性不被重视,魔数泛滥可能造成重复和发散式变化
    • 解决:相互关联的数据,有必要抽到数据对象中而不是都使用基本数据类型。常量也应该分门别类。
  • 平行继承
    • 范例:当为一个类增加一个子类,必须为另一个类增加相应子类
    • 问题:发散式变化
    • 解决:引用代替继承
  • 狎昵关糸
    • 范例:领域不清,造成太多变量、方法无法private
    • 问题:违反迪米特法则(对别的对象保持最少的了解)领域边界问题
    • 解决:划清界限,把关注点提出到新类
  • 被拒绝的馈赠
    • 范例:给子类暴露了一堆用不上的属性、方法
    • 问题:领域边界问题
    • 解决:不该暴露给子类的要坚决隔离
  • 纯数据类
    • 范例:没有方法的JavaBean
    • 问题:过度设计
    • 解决:担负更多工作,能对数据进行自管理
过度设计
  • 夸夸其谈的未来性
    • 范例:多余的抽象
    • 问题:冗余
    • 解决:用不到就别挡路
  • 中间人 委托的过度使用
    • 范例:一群中间商赚差价(Adapters)
    • 问题:过度设计
    • 解决:精简架构。
  • 不完美的库类
    • 范例:抽出来做库却没人去复用
    • 问题:过度设计。
    • 解决:评估复用性
  • 吐血新的
    • 一个类里面不要做那么多事
    • 不要用魔数
    • 不要用标志位来区分行为
    • OSD PowerProcess VideoRecordeService
    • 不要滥用静态方法
    • switch的位置 越前面越好
第三步 实践重构方案 重新组织方法
  • 提出方法 Extra method
    • 动机:主要想要注释的代码段 都应提出;保证方法的职责单一
  • 内联方法 Inline method
    • 动机:与Extra method恰恰相反。当一个方法就只有一行代码,同时又未被复用,同时即便没有名字功能也足够清晰时没必要单独存在
  • 内联临时变量 Inline temp
    • 参考Inline method
    • 如果需要让人知道其中作用可以提出到方法(?
  • Query方法取代临时变量
    • 与 Inline method,Inline temp恰恰相反。有一种情况是你需要一个方法名来作为注释
    • 为什么不用变量?因为临时变量无法被复用
  • 引入解释性变量
    • 好吧,又跟上一点矛盾了。因为我根本不可能去复用它
  • 分解临时变量
    • 除了记录循环(fori),收集结果两种情况的临时变量,都不应被二次赋值,否则往往意味着承担了一项以上职责
  • 移除对参数的赋值
    • 类似上一条原则
  • 方法对象取代方法
    • 原则 只要将相对独立的代码从大型函数中提取出来,就可以大大提高代码的可读性
    • 但局部变量的存在会影响我们提出方法的可行性。那么此时你就需要把方法和局部变量一起提出到一个新的对象里。
  • 替换算法
    • 即优化简化算法,没什么好讲的
在对象之间搬运特性
  • 搬运方法
    • 将旧方法变成一个单纯的委托方法或搬运旧方法到更需要它的类
  • 搬运字段
    • 正常来说字段不应该为public 如果必须是public 则应搬运到字段类
  • 提出类 Extra class
    • 让类的职责更单一;让类得到更好的抽象和复用
  • 内联类
    • 与上一点相反。如果提出并非必要(本身功能不够独立)
  • 隐藏委托关系
    • 委托关系只应该在委托,被委托双方之间发生耦合,不应该被场景类耦合(三端耦合)
    • MVC就是典型的三端耦合
  • 移除中间人
    • 与上一点相反,如果被委托方变成单纯的中间人,则可以考虑移除
    • 打个比方,如果View和Model简单到可以直接相互调用,根本不需要再经过一个Presenter
  • 引入外部方法
    • 当一个类没必要对某项业务亲自去实现时
    • 打个比方,吃蛋糕的人没必要去知道蛋糕怎么做
  • 【重构--改善既有代码的设计】引入本地扩展
    • 很遗憾,除了小明真的没人对蛋糕感兴趣,那还是他自产自销吧
重新面对数据
  • 封装字段 / 自封装字段
    • 即setter / getter
    • 相比直接暴露字段,可以做更多前置操作,如懒加载、前置处理(过滤等)
  • 对象取代数据
    • 一开始你以为你只需要一项数据,到后来你发现它跟多项数据关联(电话、区号、归属地、姓名等等)
    • 当关联数据逐渐庞大,对象更易管控,程序员的思维总是树形的
  • 对象取代数组
    • 有时数组的index意味着协议(事先约定),我们很难去记住属于第一位是人名这样的约定(举例McuCommand)
  • 复制被监视数据
    • 用观察者模式同步可能被多处引用的数据(比如更新ui)
  • 单向关联改为双向关联
    • 添加反向引用,可以同时更新双方状态(经典双向关联 V - P)
  • 双向关联改成单向
    • 与上一点相反,问题在于容易发生泄露
  • 常量取代魔数
    • 基本的。让人能读懂那个该死的数字是啥。同时避免发散式变化
  • 封装集合
    • 增加可读性
    • 通过Type去避免混用
  • 数据类取代记录
    • 把相关的记录整理到一个数据类中,便于后续扩展出处理这些数据的方法
  • 类型代替类型码
    • 缺乏关联性,甚至如果重了还会导致业务漏洞
  • 子类、策略对象、状态对象取代类型码
    • 如果类型码决定了业务行为,那可以直接通过子类取代类型码来避免过多的switch和代码杂糅
    • 状态模式、策略模式
简化条件表达式(条件表达式本来就是复杂而容易出错的地方)
  • 分解条件表达式
    • 条件逻辑已经很复杂了,不要在条件分支下写一堆代码。而且分支本身就表明了一定含义
    • 每个分支的执行都提出单独方法,给与名字
  • 合并条件表达式
    • 执行相同逻辑的分支,把条件合并到一个分支,并把判断条件提炼成一个方法
  • 合并重复条件
    • IDE都会提醒
  • 移除控制循环标记
    • 用break和return去控制循环退出,提高可读性
  • return取代条件表达式嵌套
    • 如题
  • 多态取代条件表达式
    • 与上一章 子类、策略对象、状态对象取代类型码 类似
  • 空对象取代Null
    • Null过于暴力,导致空指针异常,并且缺乏可控性。
  • 引入断言
    • 避免参数异常导致问题
简化方法调用
  • 方法改名
    • 给你的方法取个好名字,让人知道它具体是做什么的,不要怕方法名过长,反正会混淆的
  • 添加参数
    • 传入执行方法需要的更多信息
    • 避免相关函数做太多重复的事
    • 但警惕引入过长,过于难懂的参数列
  • 移除参数
    • 可能根本用不上这个参数,直接移除
    • 参数难以理解,移除
  • 查询方法(getter)和修改方法(setter)分离
    • 修改方法是产生副作用的,查询方法不应该产生副作用。
    • 明确职责,避免遇到线程和并发带来的问题
  • 参数对象 / 方法 取代参数
    • 保持对象完整,使参数更稳固,可读性更好
    • 用方法取代参数就更提高了可读性和可扩展性
    • 避免数据泥团
  • 移除不必要的setter
    • 如果不希望字段改变,不要个setter
  • 隐藏方法
    • 没必要暴露的方法不要暴露
  • 工厂方法取代构造方法
    • 方便扩展构建行为(单例、对象池等)
  • 封装向下转型(downcast)
    • 就是尽早的让对象有明确的类型,单一职责
  • 异常代替错误码
    • 能清晰的让你看到那些地方是异常处理
  • 测试取代异常
    • 与上一条有些许相悖
    • 主要避免异常被滥用
处理抽象关系
  • 提炼接口
    • 依赖抽象=依赖稳定
  • 字段上移
    • 子类之间是平级关系,不应该互相依赖彼此的字段
  • 方法上移、提炼超类
    • 如果是通用的方法,提到超类中实现,避免重复代码
  • 构造方法本体上移
    • 与上一条类似,避免重复代码
  • 字段、方法下移、提炼子类
    • 与以上相反
    • 如果超类的方法,根本不在部分子类关心的被调用的范畴,应该提到一个中间超类或下移到关心它的子类
  • 折叠继承
    • 折叠过度设计的继承
  • 塑造模板方法
    • 模板模式
  • 委托取代继承
    • 子类根本不完全使用超类的所有特性,把超类换成委托对象来完成业务即可,避免过度设计
  • 继承取代委托
    • 即上一条的反例

    推荐阅读