《重构》
第一章 重构,第一个示例
重构的第一步
重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验的能力。
重构过程的精髓
重构过程的精髓,小步修改,每次修改后就运行测试。(书里可以看到,每次,甚至只有变量改动的时候,都会执行编译、测试、提交)。
作者的命名习惯
命名习惯:永远将函数的返回值命名为result,使用动态类型语言时,为参数取名时默认带上类型名。
重构与性能优化
-
重构测试失败
当重构过程有测试失败,而又无法马上看清问题所在并立即修复时,回滚到最后一次可工作的一觉,然后以更小的步子重做
疑问:
A:以查询代替临时变量(178)配合内联变量(123)时(P12),虽然减少了临时变量的数量,但是增多了一个方法,这真的有必要吗?
Q:「移除局部变量的好处是做提炼时会简单得多,因为需要操心的局部作用域变少了」
A:甚至为了减少局部变量,或者说局部变量的声明更显式,增加了一步循环…………
Q:「我知道很多人本能的警惕重复的循环,但大多数时候,重复一次这样的循环对性能的影响都可忽略不计。…软件的性能通常只与代码的一小部分相关,改变其他的部分汪汪队整体性能贡献甚微。…因为有了一份结构良好的代码,回头调优其性能也容易的多」
第二章 重构的原则
名词解释
- 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
- 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
- 可观察行为:重构之后的代码所做的事应该与重构之前大致一样。
摘抄
重构在于小步迭代
重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。因此,在重构的过程中,我的代码很少进入不可工作的状态。即使重构没有完成,我也可以在任何时刻停下来。
重构与性能优化
重构是为了让代码“更容易理解,更易于修改”。这可能让程序运行的更快,也可能让程序运行的更慢。 在性能优化时,我只关心让程序运行的更快,最终得到的代码可能更难理解和维护。
重构的目的
- 重构改进软件的设计:消除重复代码
- 重构使软件更容易理解:理解并不只是对计算机,还应该是对另一个程序员,或者未来的你自己。
- 重构帮助找到bug:更加深入的理解代码的所作所为
- 重构提高编程速度:通过投入精力改善内部设计,我们增加了软件的耐久性,从而可以更长时间的保持开发的快速。
何时重构
- 预备性重构:让添加新功能更容易。
- 在开始开发新功能之前,先思考现有的结构是否满足新功能的需求,不满足就进行重构。
- 帮助理解的重构:使代码更易懂
- 捡垃圾式重构:我已经理解代码在做什么,但发现他做的不好。
有计划的重构和见机行事的重构
每次要修改时,首先令修改很容易(警告,这件事有时会很难),然后在进行这次容易的修改。 ———— Kent Beck
长期重构(不要破坏代码结构,保持过去的功能可用)
- 复审代码时重构
何时不应该重构
- 如果我看见一块凌乱的代码,但并不需要修改它。
- 如果丑陋的代码被隐藏在一个API下,我就可以忍受它继续保持丑陋,只有当我需要理解其工作原理时,对其进行重构才有价值。
- 重写比重构还容易时。
重构的意义
重构的意义不在于把代码库打磨的闪闪发光,而是纯粹经济角度出发的考量。 我们之所以重构,是因为它能让我们更快——添加功能更快,修复bug更快。 重构应该总是由经济利益驱动。
面对遗留的代码时
- 遗留系统缺少测试。
- 买一本《修改代码的艺术》。总结:建议先找到程序的接缝,在接缝处插入测试,如此将系统置于测试覆盖之下。
YAGNI/简单设计/增量式设计
与其猜测未来需要哪些灵活性、需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件,同时把软件的设计质量做的很高。随着对用户需求理解的加深,我会对架构进行重构,使其能够应对新的需要, YAGNI并不是完全不用预先考虑架构,而是将架构、设计与开发过程融合的一种工作方式,这种工作方式必须有重构作为基础才可靠。
YAGNI设计方法的三大实践
- 自测试代码
- 持续集成
- 重构
重构与性能
先写出可调优的软件,然后调优它以求获得足够的速度
第三章 代码的坏味道
1. 神秘命名
- 改变函数声明(124)
- 变量改名(137)
-
2. 重复代码
同一个类的两个函数含有相同的表达式 —— 提炼函数(106)
- 重复代码只是相似而不是完全相同 —— 移动语句(233) 重组代码顺序,把相似的部分放在一起以便提炼
重复的代码位于一个超类的不同子类中 —— 函数上移(350) 避免在两个子类之间互相调用
3. 过长函数
我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。 哪怕替换后的函数调用比函数本身还长,只要函数名称能够解释其用途,我们也该毫不犹豫的这么做,关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
提炼函数(106) —— 找到函数中适合集中在一起的部分,将他们提炼出来形成一个新函数
- 函数内有大量的参数和临时变量,对函数提炼造成阻碍 —— 查询取代临时变量(178) 消除临时元素, 引入参数对象(140) 保持对象完整(319) 将过长的参数列表变得更简洁一些。
- 杀手锏:以命令取代函数(337)
如何确定该提炼哪一段代码呢?寻找注释。如果代码前方有一行注释,就是在提醒你可以把这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,也可以这么做
4.过长参数列表
封装变量(132) ,并最好将这个函数以及其封装的数据搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域
6. 可变数据
封装变量(132) —— 确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进
- 一个变量在不同时候被用于存储不同的东西 —— 拆分变量(240) 将其拆分为各自不同用途的变量,从而避免危险的更新操作
移动语句(223) 和 提炼函数(106) —— 尽量把逻辑从处理更新操作的代码中搬出来,将没有副作用的代码与执行数据更新操作的代码分开
7. 发散式变化
含义:某个模块经常因为不同的原因在不同的方向上发生变化
- 每当对某个上下文做修改时,我们只需要理解这个上下文,不必操心另一个,也就是「每次只关心一个上下文」,这非常重要。
- 发生变化的两个方向自然的形成了先后次序 —— 拆分阶段(154) 分开两者,用一个清晰的数据结构进行沟通
- 如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后 —— 搬移函数(198)把处理逻辑分开
- 如果函数内部混合了两类处理逻辑 —— 提炼函数(106)将其分开,然后做搬移
-
8. 霰弹式修改
含义:当遇到某种变化,你需要在许多不同的类内做出许多小修改。
- 搬移函数(198) 搬移字段(207) —— 把所有需要修改的代码放进同一个模块里
- 如果很多代码都在操作相似的数据 —— 函数组合成类(144)
- 如果函数的共同功能是 转化 或 充实数据结构 —— 函数组合成变换
- 如果函数的输出可以组合成一段专门使用这些计算结果的逻辑 —— 拆分阶段(154)
内联函数(115) 或 内联类(186) —— 将本不该分散的函数拽回一处
9. 依恋情结
含义: 一个函数与另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流
如果一个函数用到了几个模块的功能,判断原则是:判断哪个模块拥有的此函数使用的数据最多,就把这个函数和那些数据摆在一起
10. 数据泥团
含义:两个类中相同的字段,许多函数签名中相同的参数
判断依据:删掉众多数据中的一项,如果这么做,其他数据如果失去了意义,这就是个 「为这些在一起出现的数据新建属于他们自己的对象」的明确信号。
11. 基本类型偏执
典型案例:用字符串表示电话号码,这种变量甚至被称为“类字符串类型”变量
- 对象取代基本类型(174)
如果想要替换的数据值是控制条件行为的类型码 —— 以子类取代类型码(362) 以多态取代条件表达式(272) 组合替换
12. 重复的switch
-
13. 循环语句
-
14. 冗赘的元素
内联函数(115) 或 内联类(186)
-
15. 夸夸其谈通用性
典型案例:函数或类的唯一用户是测试用例
- 如果某个抽象类其实没太大作用 —— 折叠继承体系(380)
- 不必要的委托 —— 内联函数(115) 或 内联类(186)
- 函数的某些参数未被用上 —— 改变函数声明(124)去掉这些参数
有并非真正需要,只是为了不知远在何处的将来而塞进去的参数 —— 改变函数声明(124) 去掉
16. 临时字段
含义:某个类的内部有一个为某种特定情况而设置的字段
提炼类(182) —— 给这个字段创建一个类,然后 —— 搬移函数(198) 把所有和这些字段相关的代码都放进这个新家
17. 过长的消息链
隐藏委托关系(189)