该文章是对《重构-改善既有代码的设计》一书的总结回顾,以时刻鞭策自己不断提高代码质量,同时供大家借鉴改善。废话不多说,马上开始。
为啥要重构
何为重构?书中的定义是:“重构就是对软件内部结构的一种调整,目的是不改变软件可观察行为的前提下,提高其可理解性,降低其可修改成本。”太过专业的解释了,通俗的来讲就是:“重构是优化代码结构,使其阅读性更好,扩展性更强的一种高级技术”。通过重构,我们使得代码更加具有扩展性可维护性。
软件开发中,随着功能的加入,程序将慢慢失去原来的结构。有些人贸然加入功能的实现代码,但却没有理解原来程序的结构和考虑代码的扩展性,程序的可读性也非常低,随着代码数量越来越多,如果不进行重构的话,程序就会越来越难以维护,导致最后放弃这个程序。我们需要重构让程序避免这样的结果。
多写测试
该书作者提倡大家多写测试代码,每完成一个功能或者做完一次重构就进行单元测试,多写测试代码不断检查自己代码的健壮性。然后书中列举了大量不好的代码示例,提醒我们在实际编程中尽量避免,接下来我们列举一下常见的需要重构的情况以及如何进行重构。
一.重构函数
重复代码
这种情况应该很多人都遇到过,编程中不要有大量的重复代码,解决办法就是去提炼到一个单独的函数中。例如下面的例子:
void A() {
.....
System.out.println("name" + _name);
}
void B() {
.....
System.out.println("name" + _name);
}
我们可以更改为:
void A() { .... }
void B() { .... }
void printName(String name) {
System.out.println("name" + name);
}
内联临时变量
顾名思义,如果你对一个变量只引用了一次,那就不妨对他进行一次重构。
int basePrice = order.basePrice();
return (basePrice > 100);
更改为:
return (order.basePrice() > 1000);
尽量去掉临时变量
临时变量多了会难以维护并对内存造成不必要开支,所以尽量去掉所使用的临时变量。
int area = _length * _width;
if (area > 1000)
return area * 5;
else
return area *4;
更改为:
if (area() > 1000)
return area() * 5;
else
return area() *4;
int area() {
return _length * _width;
}
引入解释性变量
跟上面的相反,如果使用函数变得很复杂,使人不明所以,可以考虑使用解释型变量了。
if ((platform.toUpperCase().indexOf("mac") > -1) &&
(brower.toUpperCase().indexOf("ie") > -1) &&
wasInitializes() && resize > 0) {
......
}
更改为:
final boolean isMacOS = platform.toUpperCase().indexOf("mac") > -1;
final boolean isIEBrowser = brower.toUpperCase().indexOf("ie") > -1;
final boolean wasResized = resize > 0;
if (isMacOS && isIEBrowser && wasInitializes() && wasResized) {
......
}
移除对参数的赋值
在函数中传入参数时,应该尽量避免对其进行更改。
int discount (int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
}
更改为:
int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (result > 50) result -= 2;
}
另外,函数中声明的临时变量最好只被赋值一次,如果超过一次就考虑再声明变量对其进行分解了。
一个函数也不应该太长,如果太长首先影响理解,其次包含的步骤太多会影响函数复用。做法是将里面的步骤提取为很多小函数,并且函数命名要体现出函数做了什么,清晰明了。
二.重构类
搬移方法
每一个放大都应该放在最合适的位置,不能随意乱放,所以很多时候,你需要考虑某个方法放在某处是否合适,最适合它的位置应该是哪里。此处的策略一般是由方法的功能所决定,该方法是为专一的类服务还是为基础业务服务。
class Class1 {
aMethod();
}
class Class2 {
}
更改为:
class Class1 {
}
class Class2 {
aMethod();
}
搬移字段
跟上面的方法一样,每一个字段、变量都应该放在其自己属于的类中,不属于这个类中的字段也需要及时移走。
class Class1 {
aField;
}
class Class2 {
}
更改为:
class Class1 {
}
class Class2 {
aField;
}
提炼一个新类
将不属于这个类中的字段和方法提取到一个新的类中。所以说在你写代码的时候一定要考虑这句话放这里是不是合适,有没有其他更合适的地方?
class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getTelephoneNumber() { ..... }
}
将上面的代码进行提取,如下:
class TelephoneNumber {
private String areaCode;
private String number;
public String getTelephoneNumber() { ..... }
}
class Person {
private String name;
private TelephoneNumber _officeNumber;
}
上面这种提炼类不一定就是合适的方式,有时候一个类不再有足够的价值的时候,我们就需要考虑提炼类的反向操作了。将类变为内联类了。
内容移动
有时候每一个子类都有声明一个字段或方法,但是父类里面却没有这个字段或方法,这时候就考虑把这个字段或方法移动到父类里面,去除子类的这个字段和方法。相反情况,如果父类有一个字段或方法,但只是某个子类需要使用,就需要考虑吧这个字段或方法移动到这个特定的子类里面了。
提炼接口
接口也就是协议,现在比较推崇的是面向接口编程。有时候接口将责任分离这个概念能发挥的淋漓尽致,把某些特性功能的方法提炼到接口中也是比较好的做法,这样其他想要这种功能的类只需要实现这个接口就行了。
三.重新组织数据
自封装字段
在一个类中访问自己的字段是不是应该把字段封装起来呢?这个每个人的观点是不一样的,把字段封装起来的好处就是:如果子类复写这个字段的getter函数,那么可以在里面改变这个字段的获取结果,这样子扩展性可能会更好一点。
private int _length. _width;
public int area() {
return _length * _width;
}
更改为:
private int _length. _width;
public int area() {
return getLength * getWidth();
}
int getLength() {return _length;}
int getWidth() {return _width};
以对象取代数值
随着开发的进行,有时候一个数据项表示不再简单了,比如刚开始只需要知道一个人的名字就行了,可是后来的需求变成了不但要知道这个人的名字还要知道这个人的电话号码,还有住址等。这个时候就需要考虑将数据变成一个对象了。
class Order {
private String name;
}
更改为:
class Order {
private Person person;
}
class Person {
private String name;
private String tel;
private String addr;
}
我们有时候需要把Person写成单利类,因为一个Person对象可以拥有很多份订单,但是这个对象只能有一个,所以Person我们应该写成单利。但有时候换成其他场景我们不能把他写成单利。这都是要视情况而定的。随意写代码要小心谨慎。
四.简化条件表达式
分解条件表达式
有时候看着一个if else语句很复杂,我们就试着把他分解一下。我想不出好的例子了,就简化一下了,各位莫怪。
if (isUp(case) || isLeft(case))
num = a * b;
else num = a * c;
更改为:
if (isTrue(case))
numberB(a);
else numberC(a);
boolean isTrue(case) {
return isUp(case) || isLeft(case);
}
int numberB(a) {
return a + b;
}
int numberC(a) {
return a + c;
}
当然实际情况可能复杂的多,这样的重构才显得有意思,这里只是让大家脑子里有一个这样的思想,以后遇见这样的情况能想起来可以这样子重构。
合并条件表达式
有时我们写的多个if语句是可以合并到一起的。
double disabukutyAmount() {
if (_seniority < 2) return 0;
if (_monbtdiable > 12) return 0;
if (_isPartyTime) retutn 0;
}
更改为:
double disablilityAmount() {
if (isNotEligibleForDisability()) return 0;
}
boolean isNotEligibleForDisability() {
return _seniority < 2 || _monbtdiable > 12 || _isPartyTime;
}
合并重复的条件片段
有时候你可能会在if else 语句中写重复的语句,这时候你需要将重复的语句抽出来。
if (isSpecialDeal()) {
total = price * 0.95;
send();
} else {
total = price * 0.98;
send();
}
更改为:
if (isSpecialDeal())
total = price * 0.95;
else
total = price * 0.98;
send();
以卫语句取代嵌套表达式
这个可能有点难以理解,但是我感觉用处还是比较大的,就是加入return语句去掉else语句。
if (a > 0) result = a + b;
else {
if (b > 0) result = a + c;
else {
result = a + d;
}
}
return result;
更改为:
if (a > 0) return a + b;
if (b > 0) return a + c;
return a + d;
是不是变得很简单,加入卫语句就是合理使用return关键字。有时候反转条件表达式也能简化if else语句。
以多态取代switch语句
这个我感觉很重要,用处非常多,以后你们写代码的时候只要碰到switch语句就可以考虑能不能使用面向对象的多态来替代这个switch语句呢?
int getArea() {
switch (_shap)
case circle:
return 3.14 * _r * _r; break;
case rect;
return _width + _heigth;
}
更改为:
class Shap {
int getArea(){};
}
class Circle extends Shap {
int getArea() {
return 3.14 * _r * _r; break;
}
}
class Rect extends Shap {
int getArea() {
return _width + _heigth;
}
}
然后在调用的时候只需要调用Shap的getArea()方法就行了,就可以去掉switch语句了。
然后我们还可以在一个方法中引入断言,这样可以保证函数调用的安全性,让代码更加健壮。
简化函数调用
首先要说明的是函数命名一定要有意思,一定要有意思,一定要有意思,重要的事情说三遍,不要随便命名,命名一个函数或者方法的时候一定要能表明这个方法是干什么的。
将参数对象化
函数或方法最好不要有太多的参数,太长的参数难以理解,容易造成前后不一致,最好将参数对象化,传入一个对象而不是几个参数。
public void amountReceived(int start, int end);
更改为:
public void amountReceived(DateRange range);
总结
下面是我做的一些小注意点的笔记,在文章的末尾顺便粘贴一下,看看加深一下脑子的印象。
- 当添加功能变得比较难的时候,就应该重构代码,先重构代码然后添加功能,重构代码应该一小步一小步的走。
- 方法要放到合适的类里面,找到自己合适的位置
- 尽量去除多余的临时变量
- 把大方法分割为很多小方法,函数内容越小越容易管理。
- 尽量使用多态。
- 不要有过长的参数,和过大的类
- 重构时修改接口,要保留旧接口,并让旧借口调用新接口。
- 出现switch就考虑使用多态来替换了。
- 尽可能的把大函数提炼成不同的小函数
- 有时候尽量使用内联函数
- 将一些临时变量用函数代替
- 当if语句中的判断表达式很多的时候,考虑使用临时变量分解
- 临时变量不应该赋值超过一次,应该使用final表示
- 移除对参数的改变,参数传进函数中不应该被改变本身的值
- 有些难以提炼的函数可以考虑使用函数对象
- 代码尽量不要过多出现if else语句
到这里就差不多了,文中只是把常用到的,比较好表述的重构方法或情况总结了一下,并没有覆盖到书中的所有情况,如果对重构非常有兴趣的话建议大家阅读原书,绝对值得阅读。