重构--改善既有代码的设计
前言
重构的意图
- 任何一个傻瓜都可以写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员
重构不产生新的功能,狭义范围来说也不修复原有的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)
- 就是尽早的让对象有明确的类型,单一职责
- 异常代替错误码
- 能清晰的让你看到那些地方是异常处理
- 测试取代异常
- 与上一条有些许相悖
- 主要避免异常被滥用
- 提炼接口
- 依赖抽象=依赖稳定
- 字段上移
- 子类之间是平级关系,不应该互相依赖彼此的字段
- 方法上移、提炼超类
- 如果是通用的方法,提到超类中实现,避免重复代码
- 构造方法本体上移
- 与上一条类似,避免重复代码
- 字段、方法下移、提炼子类
- 与以上相反
- 如果超类的方法,根本不在部分子类关心的被调用的范畴,应该提到一个中间超类或下移到关心它的子类
- 折叠继承
- 折叠过度设计的继承
- 塑造模板方法
- 模板模式
- 委托取代继承
- 子类根本不完全使用超类的所有特性,把超类换成委托对象来完成业务即可,避免过度设计
- 继承取代委托
- 即上一条的反例
推荐阅读
- 2018-12-03-新手教程重构思路
- 我打败了卵巢囊肿,改善了各种亚健康问题
- Swift|Swift 重构(URL 参数增删改查)
- 爱心兑换
- 仓储实务讨论(改善)(22.|仓储实务讨论(改善):22. 现场评估)
- 重构读书笔记-11_8_Extract_Interface
- 重拾夫妻信任,找到女性能量,改善母女关系,短短四周竟有这么大改变!
- 面对自己,改善自己
- 为了实践微前端,重构了自己的导航网站
- 深入学习SSR|深入学习SSR , NextJS 一起重构 掘金!