1、没有银弹
当前正在做一件所谓重构的事,遂温习一下《重构》。
人月神话中指出,在软件开发中没有银弹。软件的复杂性及其易变性,让绝对完美设计的可能性变成了不存在。高级编程语言也许是一枚铜制子弹, 但是仍然无法变成银弹。重构也不是灵丹妙药,重构也绝不是所谓的“银弹”。但重构肩负着一项特殊的使命:它和代码设计彼此互补。
2、为什么开发者不愿意重构他们的程序
为什么还不肯重构你的程序呢?这是《重构》作者的灵魂之问。
- 你不知道如何重构。
- 如果这些利益是长远的,何必现在付出这些努力呢?
- 代码重构是一项额外的工作, 老板付给你钱, 主要是让你编写新功能。
- 重构可能会破坏现有的程序。
3、重构的目标与原则
重构的基本原则应该是绝不增加也不减少功能。
目标
- 消除重复代码;
- 是软件更容易理解;
- 帮助找出bug;
- 提高编程速度;
原则
- 消除味道:一个重构应该是从识别一个坏味道(Bad Smell)开始,以消除一个坏味道结束,任何不以消除坏味道为目标的重构都是耍流氓。
- 始终工作:即重构定义中的“在不改变软件可观察行为的前提下”,说白了就是重构过程不能破坏甚至改变软件外在功能。
- 持续集成:不需要为重构单建分支,重构过程可以做到Feature开发在同一分支上持续集成持续交付。
- 随时中止:例如一个方法重命名,需要修改100个调用点,当改到50个的时候有个紧急的Feature,我可以随时暂停重构,立即切换到Feature开发上,且不需要回滚已做的重构。
- 断点续传:还是上边的例子,假如我已经完成了紧急Feature的开发,可以随时继续之前的重构,完成剩下50个调用点的重命名。
- 过程可逆:对于重构,经常有人会问:你怎么保证重构就会更好而不是更坏呢?重构的伟大就在于他跳出了对错之争,将关注点放到如何快速平滑安全的变化上,当然也包括反向重构。所以我的回答是:无法保证,但是我可以一分钟就重构回来。如果仔细看,《重构》书里的所有重构手法都是双向的,比如“Extract Method”和“Inline Method”。
作者:ThoughtWorks中国
链接:https://www.zhihu.com/questio...
4、何时重构、如何重构
关于重构的时间点,我认为重构应该由程序员主动向项目经理提起,而不是代码已经出现大量问题的时候,才由项目经理发现,并进行大规模的代码重构,《重构》指出一般有以下几个时间点,可以重构:
- 添加新功能时重构;
- 修补错误时重构;
- 复审代码时重构;
关于重构我同样更愿意走“极简主义”的路线。重构的手法大多比较底层:
- 建立或删除一个类、函数、变量;
- 修改变量、函数的访问权限, 修改函数参数;
- 在类之间移动变量,函数。
偶尔会用到比较高级的手法:
- 建立抽象超类;
- 通过继承和“简化条件”等方式简化一个类;
- 从现有部分分解出一部分,新建一个可用的组件类。
5、重构的切入点
5.1 关于注释
在编程水平、以及英文水平参差不齐的当前中国, 我认为,由于我们所使用的汉语言体系,所谓的代码即注释的言辞就是耍流氓。每个方法必须有详细的参数说明和方法用途,必要的逻辑必须清晰的描述注释。
5.2 关于细节
5.2.1 重复代码
-
对于同一个类中的重复代码,应该抽取到当前类的一个拥有详细注释的函数中。
-
对于互为兄弟的子类的重复代码, 可以考虑将公用方法推入他们的超类中, 亦可考虑模板方法模式。
-
对于好不相关的两个类, 可以考虑将重复代码提炼到一个新的类中,在原有的类中创建对象并调用。
5.2.2 过长函数
一个不熟悉面向对象编程的人常常会觉得面向对象中是无穷无尽的委托, 没有任何的计算, 实际上,小函数带来的是对代码的解释能力, 代码逻辑的共享能力以及封装的选择能力。《重构》中写道, 小函数易于理解在于一个好名字,在中文环境下,应该再加上一点——注释。越大的函数越难以理解, 那么如何将过长的函数进行分解呢?
需要写注释的时候,应该分解函数, 就算只有一行代码也应该分解到一个函数中去。那么如何抽取函数呢?
抽取函数:
(1)没有局部变量的可以直接提取
(2)有局部变量的可以把局部变量当做参数传递给新函数;
(3)有局部变量的可以把使新函数的返回值赋值给局部变量。
5.2.3 过大的类
如果一个类做了太多的事情,那往往重复代码就会接踵而至,从命名上看, 如果某个类中有多个变量有着相同的前缀或后缀, 那么就很可能意味着, 这些内容可以提炼到一个类中。那么如何来提炼一个类呢?
提炼一个同级的类
1. 决定新的类所负的责任;
2. 创建新的类, 并对原有的类更名;(如果原有类名不足以体现当前的作用)
3. 创建旧类对新类的引用; (如果需要也同时创建新类对旧类的引用)
4. 搬移字段和方法;
5. 决定新类的限定符,private.... final
提炼一个子类
1. 为源类定义一个新的子类;
2. 让子类的构造函数和源类的构造函数一致;
3. 找到调用超类的所有地点,改成调用子类。
这种做法经常用在对jar包中的类进行扩展或重写的时候。
5.2.4 过长的参数列
过长的参数列会导致调用时,参数列难以理解,如果你需要更多的数据就需要增加更多的参数, 这又会使程序更加难以理解。那么过长的参数列该如何重构呢?相信大家心里已经有数了:
引入参数对象
以函数取代参数
5.2.4 发散式变化与霰弹式变化
发散式变化
如果一个类因为一种需求的得个改变,需要改动很多方法,那么这个类就应该拆分。
霰弹式变化
如果一个需求的改变, 需要改动很多的类, 那么这些类就应该合并。
5.2.5 依恋情节与数据泥团
依恋情节
对象技术的全部要点在于:这是一种“将数据与对数据的操作行为包装在一起”的技术。
但是我们经常会面临这样的问题, 在一个函数中疯狂的调用另一个对象中的一半以上的函数, 而所使用的数据却在当前的类中,这样应该考虑函数搬家的操作了, 如果这样的操作涉及了多个类, 那么我们可以考虑策略模式或者访问者模式。
数据泥团
如果一个类中有过多的数据, 就应该考虑拆分类。
5.2.6 Switch Statements
一旦出现 Switch 就应该考虑用多态代替Switch。
考虑将Switch 抽取到一个函数中。
5.2.7 冗余类
如果一个类的对象仅仅被另外一个(仅且一个类所引用)类,那么应该考虑使用内部类, 从而去掉冗余类。
6、重构手法
7、安全重构
重构工具
对于java 程序的重构,我们几乎有着完美的重构工具。我们可以借助编译器,分析我们即将重构的内容涉及的作用域、类型和程序语义。
例如:我将要删除一个变量,IDE会告诉我将要影响的内容。你是否可以安全的删除。
对于“极简重构”,我们需要保证的是每个简单的步骤都是安全的,并且我们的重构过程没有改变程序的逻辑结构,那我们就可以认为我们的复杂重构是安全的。
安全重构的保障
1、相信你自己的编码功力;
2、相信编译器能捕捉你遗漏的错误;
3、相信代码复审;
4、相信测试人员及测试工具。
结语
如果能在软件开始时有一个良好的设计,并在恰当的时候进行重构,或许会对软件开发效率有数量级上的提升, 或许可以发现银弹。