重构 改善既有代码的设计
Refactoring Improving the Design of Existing Code
- 如果你发现自己需要为程序添加一个新特效,而代码结构使你无法很方便地达成目的,那就先重构它。
- 重构前,先检测自己是否有一套可靠的测试机制,这些测试必须有自我检验能力。
- 重构技术就是以微小的步伐修改程序,如果你犯下错误,很容易便可发现它。
- 任何一个人都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。
2.重构原则
2.1为何重构
- 重构改进软件设计
设计不好的程序需要更多的代码,因为在不同的地方使用完全相同的语句做了同样的事,一旦修改将会是灾难。 - 重构使软件更容易理解
理解程序的高层目标。 - 重构帮助找到bug
- 重构提高编程速度
重构通过阻止代码腐烂变质来节约开发成本。
2.2何时重构
- 事不过三,三则重构
- 添加功能前重构
如果用某种方式来设计,添加特效会简单的多,那么就重构它。 - 修补错误时重构
如果代码不能清晰的找到bug,说明你的代码还不够清晰,先重构它。 - 复审代码时重构
计算机科学是这样一门学科:它相信所有问题都可以通过增加一个间接层来解决。——Dennis DeBruler
3.代码的坏味道
3.1重复代码(Duplicated Code)
- 同一个类的两个函数含有相同的表达式
提取方法(Extract Method) - 互为兄弟的子类内含相同表达式
先分别为子类提取方法,然后提升方法(Pull Up Method)到超类。如果只是相似而不完全相同,则先提取出相同部分作为一个方法,然后塑造模板方法(From TemplateMethod)获得模板方法设计模式。 - 毫不相关的类出现重复代码
对其中一个类提炼类(Extract Class),然后在其它类中使用这个类。
3.2过长函数(Long Method)
“间接层”带来的好处——解释能力、共享能力、选择能力——都是有小型函数支持的。
原则:每当觉得需要以注释来说明点什么的时候,就将要说明的东西写入一个独立函数中,并以其用途(而非实现手法)命名。
- 大多数情况
使用提取方法即可 - 函数有大量的参数和临时变量
此时如果使用提取方法最终会带来许多参数给提炼的新方法。此时可以使用以查询替换临时变量(Replace Temp With Query)来消除变量。使用引入参数对象(Introduce Parameter Object)和保持对象完整(Preserve Whole Object)来使参数列变简洁。如果参数和临时变量还是很多,可以再使用以函数对象取代函数(Replace Method with Method Object)。
如何确定该提取哪一段代码?- 寻找注释,注释提示你讲这段代码替换成一个函数;
- 条件表达式,分解条件表达式(Decompose Conditional);
- 循环,应该将循环和其内的代码提炼到一个独立函数中。
3.3过大的类(Large Class)
单一职责原则 —— 面向对象设计原则
如果一个类的职责过中,往往会出现太多实例变量,同时重复代码也接踵而来。
- 将相关联的变量提炼到新类,如果发现新类适合作为子类就提炼子类(Extract Subclass);
- 如果类并非在同一时刻都使用所有的实例变量,可以多次提炼到类或者提炼到子类;
- 如果类有过多的代码,先确定如何使用它们,然后运行提炼接口(Extract Interface),为每一种使用方式提炼一个接口。这可以帮助你分解这个类。
- 如果这个类是GUI类,先将数据和行为提炼到独立的领域对象中区,然后复制观察数据(Duplicate Observed Data),构造一个观察者模式。
3.4过长参数列
在编程基础C语言上,可能老师教导:把函数所需的所有东西都以参数传递进去。因为除此之外就只能选择全局变量了,而全局变量是邪恶的东西。
面向对象技术改变了这种情况,如果你缺少东西,总可以从另一个对象中获得。
- 如果向已有对象(类内一个字段或者另一个参数)发出一条请求就可以取代一个参数
用方法代替参数(Replace Parameter with Method)。 - 有来自同一个对象的数据
使用保持对象完整(Preserve Whole Object)操作,将来它们收集起来,并替换它们。 - 参数缺乏合理的对象归属
引入参数对象(Introduce Parameter Object)。 - 例外
如果明显不希望引入某种依赖关系,可以将数据从对象中拆解出来单独作为参数。但是如果代价是参数列太长或变化太频繁,还请三思。
3.5发散式变化(Divergent Change)
Divergent Change——一个类受多种变化的影响。
针对某一外界变化的所有相应修改,都只应该发生在单一类中。为此,应该找出特定原因而造成的所有变化,然后运用提炼类。
3.6霰弹式修改(Shotgun Surgery)
Shotgun Surgery——一种变化引发多个类相应修改。
使用移动方法(Move Method)和移动字段(Move Field)把所需要修改的代码放进同一个类(没有就造一个)。通常使用内联类(Inline Class)把一系列相关行为放入同一个类。
3.7依恋情节(Feature Envy)
对象技术要点:将数据和对数据的操作行为包装在一起。
- 函数对某个类的兴趣高于自己所处类
这种羡慕之情通常的焦点是数据。把这个函数移到它该去的地方。 - 函数中一部分需要操作另一个对象的许多取值函数
使用提炼方法,将这部分提炼到独立函数中,在移到函数(Move Method)到它的梦想之地。
3.8数据泥团(Data Clumps)
Data Clumps:两个类中相同的字段、许多函数签名中相同的参数。
- 相同的字段
使用提炼类将它们提炼到一个独立对象中。 - 相同的函数参数
引入参数对象或者保持完整的对象来为函数签名减肥。直接好处就是缩小了参数列表,简化函数调用。
3.9基本类型偏执(Primitive Obsession)
到多少编程环境都有基本数据类型和结构类型。对象技术模糊了基本类型和体积较大的类之间的界限。你可以编写许多与基本类型同样轻量级的类。
- 特殊的数据值
使用对象代替数据值(Replace Data Value With Object)。 - 数据值是类型码,且它不影响行为
使用类替换类型码(Replace Type Code With Class)。 - 数据值是类型码,且有与类型码相关的条件表达式
使用子类替换类型码(Replace Type Code With Subclass)或者使用策略模式/状态模式替换类型码(Replace Type Code With State/Strategy)。 - 一组总是同时出现的字段
提炼类。 - 参数列中很多基本型数据
引入参数对象。 - 需要从数组中挑选数据
使用对象替换数组(Replace Array with Object)。
3.10Switch惊悚(Switch Statements)
switch的问题在于重复,同样的switch语句散布在不同地点。
大多数时候,可以用多态来替换switch语句。步骤如下:
- 使用提炼方法,将switch语句提炼到一个独立函数中;
- 移动方法,将提炼的方法移动到需要多态的类中;
- 决定使用子类还是使用策略/状态模式替代类型码;
- 使用多态代替条件语句(Replace Conditional with Polymorphism)。
switch语句只存在于单一函数中:
使用明确的函数替换参数(Replace Parameter with Explicit Methods)。
3.11平行继承体系(Parallel Inheritance Hierarchies)
Shotgun Surgery的特殊情况:每当为一个类增加子类,必须相应的为另一个类增加一个子类。
消除该坏味道的策略:让一个继承体系引用另一个继承体系的实例,继续移动方法和移动字段,就可以将引用端消弭于无形。
3.12 冗赘类(Lazy Class)
每一个类都需要化时间维护它,如果它不值其身价,它就应该消失。
- 子类没有足够的工作
折叠继承体系(Collapse Hierachy)。 - 几乎没用的组件
内联类(Inline Class)。 - 预想的变化,实际不会发生。
删除它。
3.13夸夸奇谈未来性(Speculative Generality)
- 抽象类没有太大用处
折叠继承体系。 - 不必要的委托
内联类。 - 函数的某些参数没有使用
移除参数(Remove Parameter)。 - 函数名称有多余的抽象意味
*重命名函数(Rename Method)。
3.14令人迷惑的临时字段(Temporary Field)
某个实例变量仅为某个特定的情况而设定,这样的代码让人难以理解。通常认为对象在所有时候都需要它的所有变量。
果类中有个复杂算法,需要好几个变量,往往会导致这个情况。
利用提炼类(Extract Class)把这些变量和相关函数提炼到一个独立的累赘,提炼出的新对象将是一个函数对象。
3.15过渡耦合的消息链(Message Chains)
对象请求另一个对象,后者再请求另一个对象…这就是消息链。一旦对象间关系变化,客户端不得不做出相应的修改。
使用隐藏委托(Hide Delegate)
3.16中间人(Middle Man)
- 如果某个类有一半的函数都委托给了其他类,就过度委托了。
移除中间人(Remove Middle Man)。 - 中间人还有其他行为
以继承替代委托(Replace Delegation with Inheritance)。
3.17狎昵关系(Inappropriate Intimacy)
两个类过于紧密。
- 将双向关联改为单向关联(Change Bidirectional Association to Unidirectional)。
- 提炼类,将两个类的共同点提炼到新类中,让它们共同使用新类。
- 继承往往造成过度亲密,运用以委托取代继承(Replace Inheritance with Delegate)。
3.18异曲同工的类(Alternative Classes with Different Interfaces)
重命名函数(Rename Method),反复运用移动函数(Move Method)将某些行为移入类,直到两者的协议一致为止。可以运用提炼父类(Extract Superclass)。
3.19不完美的类库(Incomplete Library Class)
- 修改类库的一两个函数
引入外部函数(Introduce Foreign Method),C#可以使用扩展方法。 - 添加一大堆额外行为
*添加本地扩展(Introduce Local Extension)。
3.20幼稚的数据(Data Class)
Data Class:除了它们的字段和字段访问器之外无其他行为。
找出取设值被其他类运用的地点,移动函数把这些调用行为移到Data Class中来,然后将这些取设值函数隐藏起来。
3.21被拒绝的遗赠(Refused Bequest)
- 子类继承父类的所有函数和数据,子类只挑选几样来使用。
为子类新建一个兄弟类,再运用下移方法(Push Down Method)和下移字段(Push Down Field)把用不到的函数下推个兄弟类。 - 子类只复用了父类的行为,却不想支持父类的接口。
运用委托替代继承(Replace Inheritance with Delegation)来达到目的。
3.22过多的注释(Comments)
如果代码有着长长的注释,是因为代码很糟糕。
运用提炼函数,如果提炼出来后还需要注释,接着运行重命名函数,如果需要注释来说明需求规格,尝试引入断言(Introduce Assertion)。
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释变得多余。