前言
任何一个傻瓜都可以写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员
重构的意图
重构不产生新的功能,狭义范围来说也不修复原有的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)
- 就是尽早的让对象有明确的类型,单一职责
-
异常代替错误码
- 能清晰的让你看到那些地方是异常处理
-
测试取代异常
- 与上一条有些许相悖
- 主要避免异常被滥用
处理抽象关系
-
提炼接口
- 依赖抽象=依赖稳定
-
字段上移
- 子类之间是平级关系,不应该互相依赖彼此的字段
-
方法上移、提炼超类
- 如果是通用的方法,提到超类中实现,避免重复代码
-
构造方法本体上移
- 与上一条类似,避免重复代码
-
字段、方法下移、提炼子类
- 与以上相反
- 如果超类的方法,根本不在部分子类关心的被调用的范畴,应该提到一个中间超类或下移到关心它的子类
-
折叠继承
- 折叠过度设计的继承
-
塑造模板方法
- 模板模式
-
委托取代继承
- 子类根本不完全使用超类的所有特性,把超类换成委托对象来完成业务即可,避免过度设计
-
继承取代委托
- 即上一条的反例
网友评论