第一章、重构,第一个案列

1.1 起点

例子

影片出租软件

Moive Rental Customer
priceCode:int daysRented:int
statement()

问题函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public string statement () {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result= "Rental Record for "+ getName () +"\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement ();

// determine amounts for each line
switch (each.getMovie().getPriceCode ()) {
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented() > 2)
thisAmount += (each.getDaysRented ()-2) * 1.5;
break;
case Movie. NEW_RELEASE:
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5:
if (each.getDaysRented() > 3)
thisAmount + (each.getDaysRented() -3) * 1.5;
break;
}

// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ( (each.getMovie () .get PriceCode () == Movie. NEW_RELEASE)
&& each. getDaysRented() > 1)
frequentRenterPoints ++;

// show figures for this rental
result +="\t" + each.getMovie().getTitle()+ "\t"+
string.valueof (thisAmount) + "\n";
totalAmount += thisAmount;

}
// add footer lines
result += "Amount owed is "+ String.valueof (totalAmount) + "\n";
result += "You earned"+ String.valueof (frequentRenterPoints) +
" frequent renter points";
return result;
}

问题:

  • 其中statement()负责输出用户租借影片的信息,但是其包含的内容过多,做了许多其他类应该做的事情
  • 后期需要修改程序的时候(增加了影片类别、计费模式、新的输出样式等),将会导致大量的重复代码和维护问题

解决:

  • 发现要添加一个新的功能,在原有的代码结构上难以完成,就应该先重构该代码,使其添加容易进行

1.2 重构的第一步

  • 为即将修改的代码建立一组可靠的测试环境,避免引入其他BUG
  • 好的测试是重构的根本
  • 测试必须有自我检验功能

理由:

  • 减少重构过程中的对比问题

1.3 分解并重组statement()

问题:

  • 首先statement()函数过长。代码块越小,代码的功能就愈容易管理,代码的处理和移动速度也就月轻松
  • 降低代码的重复量

解决:

  • 将逻辑泥团,提炼函数(Extract Method)。比如switch语句
  • 每次修改都应该检测一次,避免意外地引入BUG(返回值错误、数据丢失等)
  • 提炼函数后,建议修改变量的名称。因为好的代码应该清晰地表达出自己的功能,变量名称是代码清晰的关键(阅读代码的对象是人类
  • 现在大部分的IDE都提供了不同程度的重构功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public string statement () {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result= "Rental Record for "+ getName () +"\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement ();

// determine amounts for each line
thisAmount = amountFor(each)

// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ( (each.getMovie () .get PriceCode () == Movie. NEW_RELEASE)
&& each. getDaysRented() > 1)
frequentRenterPoints ++;

// show figures for this rental
result +="\t" + each.getMovie().getTitle()+ "\t"+
string.valueof (thisAmount) + "\n";
totalAmount += thisAmount;

}
// add footer lines
result += "Amount owed is "+ String.valueof (totalAmount) + "\n";
result += "You earned"+ String.valueof (frequentRenterPoints) +
" frequent renter points";
return result;
}


private double amountFor(Rental aRetal) {
double result = 0;
switch (aRetal.getMovie().getPriceCode ()) {
case Movie.REGULAR:
result += 2;
if (aRetal.getDaysRented() > 2)
result += (aRetal.getDaysRented ()-2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += aRetal.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5:
if (aRetal.getDaysRented() > 3)
result + (aRetal.getDaysRented() -3) * 1.5;
break;
}
return result;
}

1.3.1 搬移“金额计算”代码

问题:

  • amountFor(),这个函数只使用了来自Rental类的信息,却没有使用Customer类的信息
  • 在大多数情况下,函数应该放在它所使用的数据的所属对象内

解决:

  • amountFor(),这个函数移动到Rental类中
  • 使用搬移函数(Move Method)
  • Retal
  • 修改函数信息(参数、变量名称、函数名称等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Retal...
double getChange() {
double result = 0;
switch (getMovie().getPriceCode ()) {
case Movie.REGULAR:
result += 2;
if (each.getDaysRented() > 2)
result += (getDaysRented ()-2) * 1.5;
break;
case Movie. NEW_RELEASE:
result += getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5:
if (getDaysRented() > 3)
result + (getDaysRented() -3) * 1.5;
break;
}
return result;
}
  • 尝试调用函数,检测代码是否正常工作
1
2
3
4
class Customer...
private double amountFor(Rental aRental) {
return aRental.getChage();
}
  • 删除amountFor()函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public string statement () {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result= "Rental Record for "+ getName () +"\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement ();

// determine amounts for each line
thisAmount = each.getChange()

// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ( (each.getMovie () .get PriceCode () == Movie. NEW_RELEASE)
&& each. getDaysRented() > 1)
frequentRenterPoints ++;

// show figures for this rental
result +="\t" + each.getMovie().getTitle()+ "\t"+
string.valueof (thisAmount) + "\n";
totalAmount += thisAmount;

}
// add footer lines
result += "Amount owed is "+ String.valueof (totalAmount) + "\n";
result += "You earned"+ String.valueof (frequentRenterPoints) +
" frequent renter points";
return result;
}
  • 删除一些多余变量thisAmount
  • 尽量删除一些临时变量,避免大量参数互相传递,导致性能等问题发生
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public string statement () {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result= "Rental Record for "+ getName () +"\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement ();

// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ( (each.getMovie () .get PriceCode () == Movie. NEW_RELEASE)
&& each. getDaysRented() > 1)
frequentRenterPoints ++;

// show figures for this rental
result +="\t" + each.getMovie().getTitle()+ "\t"+
string.valueof (each.getChange()) + "\n";
totalAmount += each.getChange();

}
// add footer lines
result += "Amount owed is "+ String.valueof (totalAmount) + "\n";
result += "You earned"+ String.valueof (frequentRenterPoints) +
" frequent renter points";
return result;
}

1.3.2 提炼“常客积分计算”代码

问题:

  • 积分的计算根据影片的类型二不同,但是不像收费规则那样变化那么多

解决:

  • 提炼函数(Extract Method)
  • 搬移函数(Move Method)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public string statement () {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result= "Rental Record for "+ getName () +"\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement ();
frequentRenterPoints += each.getFrequentRenterPoints()

// show figures for this rental
result +="\t" + each.getMovie().getTitle()+ "\t"+
string.valueof (each.getChange()) + "\n";
totalAmount += each.getChange();

}
// add footer lines
result += "Amount owed is "+ String.valueof (totalAmount) + "\n";
result += "You earned"+ String.valueof (frequentRenterPoints) +
" frequent renter points";
return result;
}

class Rental...
int getFrequentRenterPoints() {
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE)
&& each.getDaysRented() > 1) {
return 2;
} else {
return 1
}
}

1.3.3 去除临时变量

问题:

  • 临时变了可能是一个问题,会导致使得函数复杂

解决:

  • 运用以查询代替临时变量(Replace Temp with Query)

  • 并利用查询函数(Query Method)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public string statement () {
Enumeration rentals = _rentals.elements();
String result= "Rental Record for "+ getName () +"\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
// show figures for this rental
result +="\t" + each.getMovie().getTitle()+ "\t"+
string.valueof (each.getChange()) + "\n";
}
// add footer lines
result += "Amount owed is "+ String.valueof (getTotalChange()) + "\n";
result += "You earned"+ String.valueof (getTotalFrequentRenterPoints()) +
" frequent renter points";
return result;
}

private double getTotalChange() {
double result = 0;
Enumeration rentals = _rentals.elements();
while(rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += each.getChange();
}
return result;
}

private int getTotalFrequentRenterPoints() {
int result = 0;
Enumeration rentals = _rentals.elements();
while(rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += each.getFrequentRenterPoints()
}
return result;
}

处理思考:

  • 大多数重构都会减少代码量,但是这次却增加了代码量
  • 还有原本只需要一次的循环,现在却需要三次,降低了性能
  • 但是,现在还无需担心重构带来的性能问题,后续会慢慢解决的
  • 编写htmlStatement()
  • 这里复用了原来的statement()函数的所有功能
  • 后期,可以使用构造模板函数(Form template Method)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public string htmlStatement () {
Enumeration rentals = _rentals.elements();
String result= "<h1>Rental for<em> "+ getName () +"</em></h1><p>\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
// show figures for this rental
result += each.getMovie().getTitle()+ ":"+
string.valueof (each.getChange()) + "</br>\n";
}
// add footer lines
result += "<p>You owed <em>"+ String.valueof (getTotalChange()) + "</em><p>\n";
result += "On this rental you earned <em>"+ String.valueof (getTotalFrequentRenterPoints()) +
"</em> frequent renter points<p>";
return result;
}
  • 这里客户可能需要修改影片的分类规则,然后与之相对应的费用、积分计算等都需要重新重构
  • 常常做的是,把因条件而异的代码替换掉

1.3.4 运用多态取代与价格相关的条件逻辑

问题:

  • 不要在另一个对象的属性基础上运用switch语句
  • 如果不得已使用,也应该放在自己的数据上使用,而不是别人的数据上使用
  • 这里暗示getChange()应该移动到Moive
  • 添加了参数:dayRented用于表示租借的天数
  • 这里选择将dayRented作为参数传递,而不用Moive,原因在于Moive是变化的复杂类型,这种变化不稳定,需要尽量控制它的影响
  • 将影片类型变化的东西全部放在影片里面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Moive...
double getChange(int dayRented) {
double result = 0;
switch (getPriceCode ()) {
case Movie.REGULAR:
result += 2;
if (dayRented > 2)
result += (dayRented -2) * 1.5;
break;
case Movie. NEW_RELEASE:
result += dayRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5:
if (dayRented > 3)
result + (dayRented -3) * 1.5;
break;
}
return result;
}

int getFrequentRenterPoints(int dayRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && dayRented > 1) {
return 2;
} else {
return 1
}
}



class Rental...
doubel getChange() {
return _moive.getChagre(_dayRented)
}

int getFrequentRenterPoints() {
return _moive.getFrequentRenterPoints(_dayRented)
}

1.3.5 终于……我们来到了继承

问题:

  • 影片有不同类型,根据不同类型做着不同的工作,但也有相似之处
  • 这里暗示我们可以建立Moive的子类,拥有不同的计费方法
  • 但是,一部影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期里修改自己所属的类
  • 这里可以引入中间层来完成,state模式
  • 这样,就可以在Price对象内进行子类化的动作,于是便可在任何必要的时候修改价格(更换对象的类型)
  • 联想到了iOS中的类簇UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];

解决:

  • 以State/Strategy取代类型码(Replace Type Code with State/Strategy)
  • 搬移方法(Move Method)
  • 以多态取代条件表达式(Replace Condition with Polymorphism )
  • 首先使用自封装字段(Self Encapsulate Filed)
  • 使用设值函数来代替
1
2
3
4
5
6
class Moive
public Moive(String title, int priceCode) {
_title = title;
// 影片类型
setPriceCode(priceCode)
}
  • 然后编译测试,确保没有破坏任何东西
  • 新建抽象Price类,设置抽象方法getPriceCode()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Price {
abstract int getPriceCode();
}

class ChildrenPrice extends Price {
int getPriceCode() {
return Moive.CHILDRENS
}
}

class NewReleasePrice extends Price {
int getPriceCode() {
return Moive.NEW_RELEASE
}
}

class RegularPrice extends Price {
int getPriceCode() {
return Moive.REGULAR
}
}
  • 修改Moive类内的”价格代号”访问函数
  • 子类可以赋值给父类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Moive...
public int getPriceCode() {
return _price.getPriceCode;
}

public void setPriceCode(int arg) {
switch (arg) {
case REFULAR:
_price = new RegularPrice();
break;
case CHILDRENS:
_price = new ChildrenPrice();
break;
case NEW_RELEASE:
_price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentExecption("Incorrent Price Code");
}
}

// 这里面就包含电影的类型
private Price _price;
  • getCharge()实施Move Method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Price...
double getChange(int dayRented) {
double result = 0;
switch (getPriceCode ()) {
case Movie.REGULAR:
result += 2;
if (dayRented > 2)
result += (dayRented -2) * 1.5;
break;
case Movie. NEW_RELEASE:
result += dayRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5:
if (dayRented > 3)
result + (dayRented -3) * 1.5;
break;
}
return result;
}



class Moive...
double getChange(int dayRented) {
return _price.getChange(dayRented)
}
  • 以多态取代条件表达式(Replace Condition with Polymorphism )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Price...
abstract double getChange(int dayRented);

class ChildrenPrice extends Price {
int getPriceCode() {
return Moive.CHILDRENS;
}

double getChange(int dayRented) {
double result = 1.5:
if (dayRented > 3)
result + (dayRented -3) * 1.5;
return result;
}
}

class NewReleasePrice extends Price {
int getPriceCode() {
return Moive.NEW_RELEASE;
}

double getChange(int dayRented) {
return dayRented * 3;
}
}

class RegularPrice extends Price {
int getPriceCode() {
return Moive.REGULAR;
}

double getChange(int dayRented) {
double result += 2;
if (dayRented > 2)
result += (dayRented -2) * 1.5;
return result;
}
}
  • 同样处理getFrequentRenterPoints(int dayRented)
  • 这里不打算设置getFrequentRenterPoints(int dayRented)为抽象方法,设置为默认行为
  • 然后在特定的子类中覆盖该方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Moive...
int getFrequentRenterPoints(int dayRented) {
return _price.getFrequentRenterPoints(dayRented);
}

class Price...
int getFrequentRenterPoints(int dayRented) {
return 1;
}

class NewReleasePrice
int getFrequentRenterPoints(int dayRented) {
return (dayrented > 1) ? 2 :1 ;
}