重构 改善既有代码的设计
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)。
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释变得多余。