Table of Contents
关于
《重构:改善既有代码的设计》马丁-福勒
第1章 重构,一个示例
- 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。
- 重构前,先检查自己是否有一套可靠的测试集,这些测试必须有自我检验能力。
- 小步修改,每次修改后就运行测试;测试成功后马上提交
- 提炼函数,可以自动化完成的重构
- 傻瓜都能写出计算机可以理解的代码,唯有能写出人类容易理解的代码的,才是优秀的程序员
- 编程时,需要遵循营地法则:保证你离开时的代码库一定比来时更健康
- 以多态取代条件表达式
- 要点
- 小步快跑
- 手法:提炼函数,内联变量,搬移函数,以多态取代条件表达式
第2章 重构的原则
- 如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的事不是重构
- 两顶帽子:添加新功能;重构
- 如果没有重构,程序的内部设计会逐渐腐败变质。当人们只为短期目的而修改代码时,他们经常没有完全理解架构的整体设计,于是代码逐渐失去了自己的结构。程序员越来越难通过阅读源码来理解原来的设计。代码结构的流失有累积效应。
- 改进设计的一个重要方向就是消除重复代码
- 代码量减少并不会是系统运行更快,但是可以将未来可能的程序修改动作变得容易得多。代码越多,做正确的修改就越困难,因为有更多代码需要理解。
- 我在这里做了点修改,系统却不如预期那样工作,因为我没有修改另一处——哪里的代码做着几乎完全一样的事情。
- 重构提高编程速度:好的设计可以持续的高效增加累积功能。但是坏的设计,只在初期比较快,后面就越来越困难了
何时重构
- 三次法则:第一次尽管放手做;第二次就会产生反感;第三次在做类似的事情,就应该重构
- 预备性重构:让添加新功能更容易
- 没有重复代码,修改只需要一次搞定
- 帮助理解的重构:让代码更易懂
- 命名
- 何时不应该重构
- 当一堆丑陋的代码能够隐藏在一个API下,就可以不用管。只有当需要理解其中原理时,对其重构才有价值
- 重写比重构还容易,就别重构了
- 挑战
- 延缓新功能开发
- 代码所有权
- 将函数改名,保留原来的申明,但是可以把旧接口标记为「不推荐使用」deprecated
- 分支
- 分支开发越久,合并就越困难
- 持续集成(CI),基于主干分支开发。每个团队成员每天至少向主线集成一次。代价是必须保证主干分支的健康状态。特性开关
- 测试:快速发现错误;完整的测试套件
- 遗留代码:大多数人觉得,有一大笔遗产是件好事,但是从程序员的角度来看就不同了。
- 数据库重构
- 自动化重构
- IDEA
第3章 代码的坏味道
- 神秘命名:函数取名,最难得两件事情之一
- 重复代码:->提炼函数
- 过长函数:尽量用小函数,而不要用常函数。个人理解:实际上写函数也可以遵循金字塔原理:最上层函数只串联多个子逻辑,子逻辑再递归,一直到可以接受的函数长度,或者单一功能。
- 原则:每当需要通过注释还说明什么的时候,就需要将这些代码写到一个独立函数中,并取一个好名字
- 过长参数列表
- 全局数据
- 可变数据:减少setXX方法。在很多地方对一个变量赋值,是一件难以追踪的事情
- 发散式变化:如果新加一个简单的改动,必须修改3个函数!!将这些变化的地方,搬移到同一个上线文(比如,同一个类中)
- 散弹式修改:没遇到某种变化,需要在许多不同的类内做许多小修改
- 依恋情节:一个模块内的函数跟其他模块交互比模块内其他函数还多
- 数据泥团:在许多地方看到相同的三四项数据,两个类中相同的字段,许多函数签名中相同的参数。提炼类
- 基本类型偏执:大量使用基本数据类型,如double,string
- 重复的switch:如果有两个以上的相同的switch or if...else... 代码块,应该用多态来取代!!
- 循环语句:以管道取代循环,函数式编程,map filter
- 冗赘的元素:只有一行代码的函数,只有一个函数的类
- 夸夸其谈通用性:如果你的某个抽象类实际没有太多用途,请运用折叠继承体系
- 临时字段:字段仅为某种特殊情况设定。在字段未被使用的时候猜测他的用途太痛苦了!
- 过长的消息链:A->B->C->D:隐藏委托关系
- 中间人:某个类一半的函数委托给其他类
- 内幕交易
- 过大的类
- 异曲同工的类:提炼超类
- 纯数据类型:移除public字段,找到设值点,尝试搬移函数,或提炼函数
- 被拒绝的遗赠:子类只想继承父类一部分东西
- 注释
第4章 构筑测试体系
- 每当你收到bug报告,请先写一个单元测试来暴露这个BUG
第5章 介绍重构名录
- 略
第6章 第一组重构
- 提炼函数:如果你需要花时间浏览一段代码才能弄清他到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事情为他命名
- 内联函数:提炼函数反向操作,删除无用的间接层
- 提炼变量:取代表达式,提高可读性
- 内链变量
- 函数改名
- 封装变量:封装一组数据
- 变量改名
- 引入参数对象:将一组数据用一个类来实现
- 函数组合成类
-
拆分阶段:一段代码有两个部分,每部分单独干一件事情,则可以拆分成两个函数,顺序执行
-
注解:一组相同的字段可以构建一个数据结构,一组总是对同一个数据结构的函数,可以构成一个类
第7章 封装
- 封装记录,以数据类型代替记录
- 封装集合,只允许读操作
- 以对象取代基本类型
- 以查询取代临时变量
- 提炼类
- 内联类
- 隐藏委托关系
manager = aPerson.department.manager;
修改为
manager = aPerson.manager; class Person{ get manager(){return this.department.manager; } }
- 移除中间人:
- 替换算法:将不好的算法替换成更好的算法,例如常见的if...else... 通过循环来实现
第8章 搬移特性
- 搬移函数,搬移函数到正确的上下文
- 搬移函数,如果修改一个数据总是要设计到另外一个对象的数据,那么说明这两部分数据应该要在一个上下文
- 搬移语句到函数,相同的语句放到一个函数
- 函数调用取代内联代码
- 拆分循环,将干两件事情的一个循环拆成两个都只干一件事情的循环。可能损失性能,但是大部分情况这些性能损失微不足道,但是拆分对可读性有较好的提升
- 以管道取代循环
第9章 重新组织数据
- 将一个变量用于多个不同的用途,这是催生混乱和bug的温床
- 拆分变量
第10章 简化条件逻辑
- 将复杂的条件和分支应用提炼函数来重构,让逻辑变清晰
- 合并分支逻辑相同的条件
- 以卫语句取代嵌套条件表达式:如果某个条件为真就返回
- 以多态取代条件表达式
- 如果现有的类不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例
- 在调用代码中使用工厂函数获得对象实例
- 将带有条件逻辑的函数移到超类中
- 选一个子类,在其建立一个覆盖超类条件表达式的函数
- 或者直接在超类抛异常
- 引入特例:null对象
- 总是在检查某个特殊的值,并且在这个值处理逻辑也一样
- 引入断言:使用断言强制要求满足某个条件才能执行
第11章 重构API
- 将查询函数和修改函数分离
- 函数参数化,将相似的函数合并,将变化的地方换成参数
- 移除标记参数,标记参数是指bool值,或枚举值,字符串等用于函数分支逻辑判断的参数。应该直接拆分到多个不同的子逻辑中
- 保持对象完整:如果一段代码从一个对象提取几个值,然后又把这几个值传个一个处理函数,那么应该将整个对象传过去
- 以查询取代参数,函数参数应该避免重复,应该保持最少的参数
- 以参数取代查询:如果因为传递整个对象,导致模块间依赖太多,可以改成传递必须的参数而不是整个对象
- 移除设置函数,避免这个值被改变
- 以工厂函数取代构造函数:要创建不同类型的子类对象时有用
- 以命令取代函数:命令是指一个对象,其中封装了一个函数对奥用请求
第12章 处理继承关系
- 函数上移:将重复的函数上移到父类
- 字段上移
- 构造函数上移
- 函数下移
- 字段下移
- 以子类取代类型码,比如各种type字段类型码
- 移除子类
- 提炼超类:将相似的字段和函数上移,提炼超类
- 以委托取代子类:继承只能使用一次,即封装一种变化???
- 以委托取代超类:因为超类的部分方法不适合在子类上,所以讲超类作为子类的一个内部对象(即委托)
FAQ
如果待重构的代码没有测试怎么办
- 添加测试用例