# 第1章:重构,第一个案例
# 一、起点
一个影片出租店的应用程序,根据顾客组的影片,计算消费金额。
影片类:
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String _title;
private int _priceCode;
public Movie(String title, int priceCode) {
_title = title;
_priceCode = priceCode;
}
public int getPriceCode() {
return _priceCode;
}
public void setPriceCode(int priceCode) {
_priceCode = priceCode;
}
public String getTitle() {
return _title;
}
}
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 class Rental {
private Movie _movie;
private int _daysRented;
public Rental(Movie movie, int daysRented) {
_movie = movie;
_daysRented = daysRented;
}
public int getDaysRented() {
return _daysRented;
}
public Movie getMovie() {
return _movie;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
顾客类:
public class Customer {
private String _name;
private Vector _rentals = new Vector();
public Customer(String name) {
_name = name;
}
public void addRental(Rental arg) {
_rentals.addElement(arg);
}
public String getName() {
return _name;
}
/**
* 生成详单
*
* @return
*/
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().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
frequentRenterPoints++;
// show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" + thisAmount + "\n";
totalAmount += thisAmount;
}
// add footer lines
result += "Amount owed is " + totalAmount + "\n";
result += "You earned " + frequentRenterPoints + " frequent renter points";
return result;
}
}
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
53
54
55
56
57
58
59
60
61
62
63
64
在上面的 statement()
方法中做了很多很多了逻辑,有的其实并不属于这个类应该处理的。加入后期需要让这个方法返回一个 html
格式的详单,那么修改这个方法是不可能的了,肯定要单独写一个返回 html
输出的方法,那么其中的计费标准如果有变动的话,那么 statement()
和 新写的这个方法都会要改动,以后就得维护两个地方,改的越多越容易出错。
如果你发现自己需要为程序添加一个特性,而代码结构是你无法很方便地打成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性
# 二、重构的第一步
由于 statement()
的运作结果是个字符串,所以先假设一些顾客租赁不同的影片,产生报表字符串,然后用这个字符串和重构后产生的字符串进行对比,看是否一致。
重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验能力
下面是我的测试类:
public class ShopTest {
@Test
public void test() {
Customer customer = new Customer("jerry");
Movie movie1 = new Movie("爱乐之城", Movie.REGULAR);
Rental rental1 = new Rental(movie1, 4);
Movie movie2 = new Movie("千与千寻", Movie.CHILDRENS);
Rental rental2 = new Rental(movie2, 2);
customer.addRental(rental1);
customer.addRental(rental2);
System.out.println(customer.statement());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
执行结果:
Rental Record for jerry
爱乐之城 5.0
千与千寻 1.5
Amount owed is 6.5
You earned 2 frequent renter points
2
3
4
5
# 三、分解并重组 statement()
将长的离谱的方法进行拆分。代码块俞小,代码的功能就俞容易管理,代码的处理和移动也就俞轻松。
首先,在方法内找到局部变量和参数,each
和 thisAmount
,前者违背修改,后者会被修改,任何不会被修改的变量都可以被当成参数传入新的方法中,至于会被修改的变量就需格外小心。如果只有一个变量会被修改,可以当做返回值。thisAmount
是个临时变量,其值在每次循环起始处被设置0,并在 switch
语句之前不会被改变,所以可以直接把新方法的返回值赋给它。
重构后的代码:
public class Customer {
private String _name;
private Vector _rentals = new Vector();
public Customer(String name) {
_name = name;
}
public void addRental(Rental arg) {
_rentals.addElement(arg);
}
public String getName() {
return _name;
}
/**
* 生成详单
*
* @return
*/
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();
thisAmount = amountFor(each);
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
frequentRenterPoints++;
// show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" + thisAmount + "\n";
totalAmount += thisAmount;
}
// add footer lines
result += "Amount owed is " + totalAmount + "\n";
result += "You earned " + frequentRenterPoints + " frequent renter points";
return result;
}
private double amountFor(Rental each) {
double thisAmount = 0;
// 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;
}
return thisAmount;
}
}
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
注意这里重构的时候可能会将 amountFor()
返回值定义成 int
,那么就会出错,实际上应当是 double
,做好测试后就会发现这个问题。
重构技术就是以微笑的步伐修改程序。如果你犯下错误,很容易便可发现它。
对于 amountFor()
方法中的变量名还不是很满意,于是对于 amountFor()
方法继续重构:
private double amountFor(Rental aRental) {
double result = 0;
// determine amounts for aRental line
switch (aRental.getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (aRental.getDaysRented() > 2)
result += (aRental.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += aRental.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (aRental.getDaysRented() > 3)
result += (aRental.getDaysRented() - 3) * 1.5;
break;
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。
继续观察发现 amountFor()
方法使用了来自 Rental
类的信息,却没有使用来自 Customer
类的信息,所以这个方法应当已到 Rental
类中:
public class Rental {
private Movie _movie;
private int _daysRented;
public Rental(Movie movie, int daysRented) {
_movie = movie;
_daysRented = daysRented;
}
public int getDaysRented() {
return _daysRented;
}
public Movie getMovie() {
return _movie;
}
double getCharge() {
double result = 0;
// determine amounts for aRental line
switch (getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (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;
}
}
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
现在就可以去掉 Customer
类中的 amountFor()
方法,直接采用 Rental
类的 getCharge()
方法了。
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();
thisAmount = each.getCharge();
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
frequentRenterPoints++;
// show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" + thisAmount + "\n";
totalAmount += thisAmount;
}
// add footer lines
result += "Amount owed is " + totalAmount + "\n";
result += "You earned " + frequentRenterPoints + " frequent renter points";
return result;
}
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
下一步,其实发现 statement()
方法中的 thisAmount
可以直接使用 each.getCharge()
来替换就可以了:
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().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
frequentRenterPoints++;
// show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" + each.getCharge() + "\n";
totalAmount += each.getCharge();
}
// add footer lines
result += "Amount owed is " + totalAmount + "\n";
result += "You earned " + frequentRenterPoints + " frequent renter points";
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
虽然这么做会在 getCharse()
中进行两次计算,但是这个计算可以在 Rental
类中进行优化。
接下来开始优化计算常客积分的这部分代码。
依旧是在 Rental
类中新增一个方法:
int getFrequentRenterPoints() {
int frequentRenterPoints = 0;
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1)
frequentRenterPoints++;
return frequentRenterPoints;
}
2
3
4
5
6
7
8
9
用于替换在 Customer
类中计算常客积分部分代码:
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" + each.getCharge() + "\n";
totalAmount += each.getCharge();
}
// add footer lines
result += "Amount owed is " + totalAmount + "\n";
result += "You earned " + frequentRenterPoints + " frequent renter points";
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
接下来处理临时变量的问题,在 statement()
方法中还存在两个临时变量 totalAmount
和 frequentRenterPoints
,首先用 getTotalCharge()
取代 totalAmount
:
public String statement() {
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" + each.getCharge() + "\n";
}
// add footer lines
result += "Amount owed is " + getTotalCharge() + "\n";
result += "You earned " + frequentRenterPoints + " frequent renter points";
return result;
}
private double getTotalCharge() {
double result = 0;
Enumeration rentals = _rentals.elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += each.getCharge();
}
return result;
}
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
然后以同样的方法处理 frequentRenterPoints
:
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" + each.getCharge() + "\n";
}
// add footer lines
result += "Amount owed is " + getTotalCharge() + "\n";
result += "You earned " + getTotalFrequentRenterPoints() + " frequent renter points";
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
现在利用现有的这些方法,可以轻松的实现一个 htmlStatement()
方法出来,并且如果计算规则发生改变,只需要在程序中做一处修改。
# 四、运用多态取代与价格相关的条件逻辑
现在在 Rental
类的 getCharge()
方法中还有一部分 switch
语句:
double getCharge() {
double result = 0;
// determine amounts for aRental line
switch (getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
最好不要再另一个对象的属性基础上运用 switch
语句。如果不得不使用,,也应该在对象自己的数据上使用,而不是在别人的数据上使用。也就是说这部分 switch
语句应当移动到 Movie
类中,若如此,就得将 getDaysRented()
作为一个参数传递到 Movie
类中将要创建的方法中去。先看 Movie
新增的方法:
double getCharge(int daysRented) {
double result = 0;
// determine amounts for aRental line
switch (getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
break;
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
计算费用时需要两项数据:租期长度和影片类型,一种方式是将租期长度传递给 Movie
计算,另一种方式是将影片类型传递给 Rental
计算,这里为什么要将租期作为参数传递给 Movie
类呢?因为系统后期可能会增加新的影片类型,这种变化带有不稳定倾向,所以选择在 Movie
对象内计算费用。
Rental
的计算方法简化为:
double getCharge() {
return _movie.getCharge(_daysRented);
}
2
3
同样的方式处理常客积分,在 Movie
类中新增方法:
int getFrequentRenterPoints(int daysRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
return 2;
else
return 1;
}
2
3
4
5
6
Rental
的计算方法简化为:
int getFrequentRenterPoints() {
return _movie.getFrequentRenterPoints(_daysRented);
}
2
3
我们有数种影片类型,它们以不同的方式回答相同的问题。这听起来像子类的工作。可以建立 Movie
的三个子类,每个都有自己的计费方法。这样一来就可以使用多态来取代 switch
语句了,这里有个问题:一部影片可以在声明周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。这里就可以使用设计模式中的状态模式,新建一个抽象类 Price
,让几种不同的计费规则继承这个类。
先将与类型相关的行为搬移到状态模式中,然后将 switch
语句移动到 Price
类中,最后去掉 switch
。
Movie
的构造方法修改为:
public Movie(String title, int priceCode) {
_title = title;
setPriceCode(priceCode);
}
2
3
4
新建 Price
类:
public abstract class Price {
abstract int getPriceCode();
}
2
3
4
在创建3个类,继承自 Price
,每个类中都提供类型相关的行为:
public class ChildrensPrice extends Price{
@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
}
2
3
4
5
6
7
public class NewReleasePrice extends Price{
@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
}
2
3
4
5
6
7
public class RegularPrice extends Price{
@Override
int getPriceCode() {
return Movie.REGULAR;
}
}
2
3
4
5
6
7
现在修改 Movie
类中访问价格代号的方法,使用新创建的类,此时 Movie
类还得新增一个 Price
对象:
private Price _price;
public Movie(String title, int priceCode) {
_title = title;
setPriceCode(priceCode);
}
public int getPriceCode() {
return _price.getPriceCode();
}
public void setPriceCode(int priceCode) {
switch (priceCode) {
case REGULAR:
_price = new RegularPrice();
break;
case CHILDRENS:
_price = new ChildrensPrice();
break;
case NEW_RELEASE:
_price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
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
在 Price
类新增一个实现好的方法:
double getCharge(int daysRented) {
double result = 0;
// determine amounts for aRental line
switch (getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
break;
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这样 Movie
中的 getCharge()
方法就可以直接引用 Price
的 getCharge()
方法了:
double getCharge(int daysRented) {
return _price.getCharge(daysRented);
}
2
3
又因为每个 Price
的子类实现计费的方法还是放在各自类中比较合理,所以将 Price
的 getCharge()
方法分发到各自子类中去吧:
public class RegularPrice extends Price {
@Override
int getPriceCode() {
return Movie.REGULAR;
}
@Override
double getCharge(int daysRented) {
double result = 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
return result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class NewReleasePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
@Override
double getCharge(int daysRented) {
return daysRented * 3;
}
}
2
3
4
5
6
7
8
9
10
11
12
public class ChildrensPrice extends Price {
@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
@Override
double getCharge(int daysRented) {
double result = 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
return result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
替换完所有的子类之后,就可以将 Price
中的 getCharge()
方法声明为抽象的了:
public abstract class Price {
abstract int getPriceCode();
abstract double getCharge(int daysRented);
}
2
3
4
5
6
同样的手法处理 Movie
中的 getFrequentRenterPoints()
方法吧。在 Price
类中新增 getFrequentRenterPoints()
方法:
int getFrequentRenterPoints(int daysRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
return 2;
else
return 1;
}
2
3
4
5
6
Movie
中就可以直接调用这个方法:
int getFrequentRenterPoints(int daysRented) {
return _price.getFrequentRenterPoints(daysRented);
}
2
3
因为 getFrequentRenterPoints()
方法中只对类型是 Movie.NEW_RELEASE
的有特殊处理,所以这次决定在 Price
类中保留一个默认实现,然后让 NewReleasePrice
类覆盖这个方法,实现它自己的逻辑即可。
Movie
类中的默认实现:
public abstract class Price {
abstract int getPriceCode();
abstract double getCharge(int daysRented);
int getFrequentRenterPoints(int daysRented) {
return 1;
}
}
2
3
4
5
6
7
8
9
10
NewReleasePrice
类覆盖 getFrequentRenterPoints()
方法:
public class NewReleasePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
@Override
double getCharge(int daysRented) {
return daysRented * 3;
}
@Override
int getFrequentRenterPoints(int daysRented) {
return daysRented > 1 ? 2 : 1;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这下,如果需要修改任何与价格有关的行为,或者是添加新的计费规则,或者是加入其它决定价格的行为,程序的修改都会容易的多。
第2章:重构原则 →