重构之Code Small 代码的坏味道
前言
本篇文章的内容是学习完《重构 改善既有的代码设计》而整理的内容,其中有一些是我在运用这些重构手法的的理解。
写代码如同写文章,好的文章总是能让人津津乐道,而坏的文章读起来味同嚼蜡。评论一篇文章的好坏,首先要看语言是否表达通畅,然后要看文章结构组织是否合理。软件编程亦如此。
重构教你如何将代码书写得通顺,那么《设计模式》就是将代码合理组织起来的内经心法,通常我会将两者搭配起来使用。我认为这两本书是相辅相成的,设计模式是众多开发人员经过相当长的一段时间实战,试验而总结出来的高度结构化的成果,完全理解他们是有一定难度的。然而,重构则是告诉你在实战中更合理有效的将代码结构化。也许你正在重构的时候,突然醒悟,你正在使用着某种设计模式。
重构
重构的概念
重构是对软件内部结构的调整,目的是在不改变软件可观察行为的前提下,提高其理解性,降低修改成本。
何时重构
重构应该是随时随地的进行了,不应该为了重构而重构。 添加新功能时重构,修补错误时重构,复审代码时重构。
Code Small 代码的坏味道
Alternative Classes with Different Interfaces. 异曲同工的类。
重构手法:
- Rename Method
- Move Method
如果两个函数做同一件事,却有着不同的签名(函数签名由参数个数及其类型组成,函数重载时根据签名确定某一个具体的函数),请使用Rename Method,根据他们的用途重新命名。
如果两个类做同一件事,却有着不同的类名,请使用Move Method 将某些行为移入到其中一个类中。或者使用Extract Superclass 完成这一项重构。
Comments. 过多的注释
重构手法:
- Extract Method
- Introduce Assertion
Comments是一件好事,甚至是一种香味,而不是Small Code。但是如果把它当做除草剂来使用,常常会有这样的情况: 你看到某些长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕,无法清晰表达作者的意思。这种情况发生多了,就会感受到香水在严重的脚臭面前的无力。
Comments出现的地方,通常是Code Small的导向,通过Extract Method 把需要解释的代码提取出来,并通过函数名来表达代码的意图,通常经过这样的重构之后,注释已经变得多余了,因为代码已经说说明了一切。
Data Class. 纯稚的数据类
重构手法:
- Move Method
- Encapsulate Field
- Encapsulate Collection
Data Class是指:他们拥有一些字段,以及访问(Get/Set)这些字段的函数。
Data Class就像小孩子,作为起点是好的,但若要让它像成熟的对象那样参与整个系统的工作,他们就必须承担一定责任。
Data Class的Code Small是因为它过于简单,稚嫩。若存在public字段,应立即用Encapsulate Field将其封装;若存在容器类的字段,需检查该字段是否得到了恰当的封装,若没有,则使用Encapsulate Collection把他们封装起来;对于那些不能被修改的字段,应使用Remove Setting Method;最后是检查调用Get/Set函数的地方,把公用的使用封装在Get/Set函数中,使其承担一定的责任。
// Encapsulate Field
public String _name;
// 改为
private String _name;
public getName();
public setName();
// Encapsulate Collection
pirvate Set _address
public getAddress();
public setAddress();
//改为
private Set _address
public getAddress(); // Unmodifiable Set
public addAddress();
public removeAddress();
Data Clumps. 数据泥团
重构手法:
- Extract Class
- Introduce Parameter Object
- Preserve Whole Object
如果总有一些数据项成群结队的待在一块儿,一起出现在不同的类中出现,又或者一起出现函数的签名中。这些总是绑在一起出现的数据就应该拥有属于它们自己的对象。
首先运用Extract Class将这些一起出现的数据项提取到一个类中,再把注意力转移到函数签名上,运用Introduce Parameter Object和Preserve Whole Object为它减肥。这么做的直接好处就是可以将很多参数列缩短,简化函数的调用。
Divergent Change. 发散式变化
重构手法:
- Extract Class
Divergent CHange是指一个类受多种变化的影响。
例如,如果新增一个数据库,我必须修改三个函数;如果出现新的一种金融工具,我必须修改四个函数。那么此时也许将这个对象分为两个比较好。这么一来每个对象就可以因为一种变化而修改。
Shotgun Surgery. 霰弹式修改
重构手法:
- Move method
- Move Failed
- Inline Class
Shotgun Surgery 是指一个变化引发多个类的修改,正好与Divergent Change相反。
这种情况你应该使用上述重构手法,将需要修改的代码放在同一个类中。
Divergent Change 和 Shotgun Surgery是两种相反的Code Small,这两种情况下你都希望整理代码,使“外界变化”与“需要的修改的类”趋于一一对象。
Duplicated code.重复代码
重构手法:
- Extract Method
- Extract Class
- Pull Up Method
- Form Template Method
Form Template Method 是设计模式中的模板方法模式,旨在将具有相似逻辑、结构、执行顺序的代码形成模板方法,每个子类只需要实现每个步骤的细节。通常与Pull Up Method 一起使用。
通常IDE会提示工程中的重复代码。当重复的代码出现时,最简单粗暴的做法就是Extract Method, 如果在其他类中也存在重复的代码,就Extract Class.
Feature Envy. 依恋情节
重构手法:
- Move method
- Move Field
- Extract Method
Feature Envy是指:实现A类中的一个函数却调用了B类中的大半部分的函数,A类的函数严重依赖于B类。
造成这类情况通常是职责的划分不够清晰,A类中的函数如此依恋B类,就应该把A类中的函数放在B类中。
但是实际情况并没有这么简单,不能很好的划分一个类具体是属于A类还是属于B类,此时需要遵循一个最根本的原则:将总是变化的东西放在一块儿。
在对该Code Small进行重构时,首先应该考虑Extract Method将职责划分不清的函数分成更小的函数,并将其放在不同的地点。
Inappropriate Intimacy 过度亲密
重构手法:
- Move method
- Move filed.
- Change Bidirectional Association to Unidirectional. 将双向关联改为单向关联
- Replace Inherihance with Delegation
- Hide Delegation
有时候你会看到两个类过于亲密,花费太多时间去探究彼此的private 成分。就像古代恋人,过分亲密的类就必须拆散。
继承往往造成过度亲密,因为子类对超类的了解总是超过超类的主观愿望。如果你认为该网这个孩子独立生活了, 请运用Replace Inheritance with Delegation 让它离开继承体系。
Incomplete Libarary. 不完整的库类
重构手法:
- Introduce Foreign Method.
- Introduce Local Extension.
当调用某些库类的时,存在库类中不存在所需要的函数。例如,时间的工具类中,不会直接提供UTC时间,也不会提供根据当前日期得到下一天的日期等等。
如果只有一次需要实现库类中不存在中的函数,可在当前类中 Introduce Foreign Method即可。
如果这个函数被多处调用,就需要Introduce Local Extension,重写不完美库类。可通过委托和继承的方式现在。
Large Class. 过大的类
重构手法:
- Extract Class
- Extract SubClass
- Extract Interface
- Replace Data Value with Object.
过大的类中通常会将很多职责混扎在一起,开发初期的代码往往是这样的,只管实现功能,把所有的东西都混在一起。继而通过上述重构解决混乱的代码。
上述过程也符合三次法则:
第一次只管去做;第二次做类似的事会产生反感,但无论如何还是可以做;第三次再做类似的事,你就必须重构了。
Lazy Class. 冗余类
重构手法:
- Inlince Class.
- Collaose Hierarchy.
这种Code Small 与 Larage Class 正好相反,采用重构手法也是相反的,这种情况通常是过度划分功能的职责。
Long Method. 过长的函数
重构手法:
- Exract Method.
- Replace Temp With Query.
- Replace Method with Method Object
- Decompose conditional.
过长的函数在可读性上比较差,是一种面向过程的编程方式,不利于代码重复利用和职责划分。
通过上述重构手法,首先,需要理清这个函数的主干,然后把主*分实现细节提取到函数的外部。
Long Parameter List. 过长的参数列表
重构手法:
- Replace Parameter with Method.
- Introduce Parameter Object.
- Preserve Whole Object.
解决过长的参数列表也需要平衡每一个参数应不应该作为一个参数。Replace Parameter with Method, 是通过函数内部去获取值,而不是通过参数传进来,如果能够在函数内部获取值,就不要通过传参的方式获取值。
但是有时候也会发现,将常量作为参数传递,这再正常不过了,如果能够减少重复代码,将常量作为参数传递就是可取的方式。
Message Chains. 过度耦合的消息链
重构手法:
- Hide Delegate.
用户请求一个对象,这个对象请求另一个对象... 实际代码中看到最终实现功能的代码被一层一层的委托。
在大项目中通常会见到这种情况,这种消息链不仅会增加系统的复杂度,同时,链中的其中一环发生变化,客户端的调用也可能跟着发生变化。
通常会将中间的链节点称为中间人,可通过Hide Delegate 去除中间人。
Middle Man. 中间人
重构手法:
- Remove Middle Man
- Inline Method
- Replace Delegation with Inheritance.
委托往往伴随着封装而生,合理的使用委托可以使代码的结构更清晰,但是过度使用委托,则会产生很多中间关系,为了委托而引入冗余的代码。
你也许会看到某个类接口有一半的函数都委托给其它类,这样就是过度运用委托。遇到这种情况就应该移除中间人呢,或者Replace Delegation with Inheritance,去除那些“不干实事”的委托动作。
Parallel Inheritance Hierarchies. 平行继承体系
重构手法:
- Move Method
- Move Faild
每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类,这是Shotgun Surgery的特殊情况。
这种Code Small 通常是为同一个功能的有多种实现分支时产生的,它们有相似的地方,也有差异的地方,如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是闻到了这种坏味道。
消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例。然后再接着用Move Method 和Move Failed将引用端的继承体系消弭于无形。
Primitive Obsession. 基本类型偏执
重构手法:
- Replace Data Value with Object
- Extract Class
- Introduce Parameter Object
- Replace Array with Object
- Replace Type Code with Class
- Replace Type Code with Subclass
- Replace Type Code with State/Strategy
大多数编程环境都有两种数据,结构类型允许你将数据组织成有意义的形式,基本类型则是构成结构类型的积木。
基本类型偏执是一种执着于使用基本类型数据,从而造成的代码逻辑的模糊。结构类型会带来一定的额外开销,但如果执着于额外的开销,偏执于基本类型,往往会造成函数中基本类型数据满天飞。
像结合数值和币种的money类,仅由一个起始值和一个结束值组成的range类,描述同一类事物的属性组合,代表类型码的数据值等等,这些具有一定关联的属性,都应该通过上面7中重构手法,将其封装成一个独立的Object。
Refused Bequest. 被拒绝的遗赠
重构手法:
- Replcae Inheritance With Delegation
子类应该继承超类的函数和数据,但是如果子类只是想用超类的其中几样函数和数据,而不想附带父类的其余函数和数据,尤其是不愿意支持超类的接口。这就像一对矛盾的父子,父亲总是把自己所有的东西给孩子,而孩子对其表示很叛逆,并不想拥有父亲的所有的东西。
这种Code Small通常是继承体系的设计错误,这种矛盾的情况出现,它的体现是明显的,运用Replace Inhertance with Delegation去除这种错误的继承体系。使用委托类,仅对外部表现需要的行为,而封装外部不需要的行为。
Speculative Generality. 夸夸其谈未来性
重构手法:
- Collapse Hierarchy
- Inline Class
- Remove Parameter
- Rename method
可能会用到,但实际没有用到,是造成这类Code Small的原因。那么做的结果往往造成了系统更加难以理解和维护。如果该实现方式有被用到,就值得留下,如果没有,就果断删掉。
造成这类原因大致有几种,一是过度设计,用复杂的设计方式完成仅简单的功能,二是一个问题有多种解决方法,在实现的过程中,用其中一种实现了,但舍不得删掉其它的,遗留在系统中,三是需求改变,为防止继续变化,多余代码遗留在系统中。程序员也是需要当机立断的呀。
识别这一些Code Small 就看类或函数的调用者,如果该类或函数从未被调用,或者只有测试类调用,请把他们连同其测试用例一并删掉。
Switch Statements.Switch 语句,或者if-else-if
重构手法:
- Replace Conditional with Polymorphism
- Replace Type Code with Subclass
- Replace Type Code with State/Strategy
- Replace Parameter with Explicit Methods.
- Introduce Null Object.
可以通过继承与多态的将Switch与分支中的代码分离,典型设计模式之策略模式和状态模式是处理Swich的方法。
在传统的桌面应用程序开发中,可以通过策略模式和状态模式完全消除Switch Stetements. 但是在BS 开发模式中,还是会在函数的顶端存在Switch Statements.一般可以通过简单的工厂模式将Switch 集中管理起来。
// 伪代码
Class Employee {
static EmployeeType create(String type) {
case1: return new Manager();
case2: return new SalesMan();
}
}
Tempoary Field. 临时成员变量
重构手法:
- Extract Class
- Introduce Null Onject.
Tempoary Variables. 临时变量
如果临时变量造成函数变得复杂,或者临时变量只是解释一个一目了然的代码,可以采用重构手法去除临时变量。
如果函数中,有需要解释的地方,也可引入临时变量。例如条件表达式中存在多个条件,可以引入临时变量保存多个条件组合的值。
总结
重构无处不在,而重构总会带来很多潜在的价值。在一个项目中,代码的可阅读性决定了项目的维护难度。而对于个人而言,再回首几个月前的代码,能由衷地感叹一句,怎么可以写这么烂,那这几个月就值了。
上一篇: 子集生成
下一篇: 浅谈带头结点的双向循环链表和单链表