重构书中例子的总结

《重构:改善既有代码的设计》这本书啊,几乎所有的精华都在这个书中最开始的例子里。本文就是对这个例子的总结。

任务描述

例子中是一个CD店,这个店有个管理系统,用于记录顾客的消费,积分并打印消费清单,计算费用等。是一个很简单的管理系统。那么最初的架构是这样的:
refactor0

时序图如下所示:
refactor1

Movie类是一个数据类,用于存放Movie的相关属性信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 setProceCode(int arg){
_priceCode = arg;
}
public String getTitle(){
return _title;
}
}

Rental用来存放对应的租赁关系,所以这个类中有两个成员_movie以及_daysRented

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}

customer类用来存放顾客信息,并记录顾客的租赁信息

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
53
54
55
import java.util.Enumeration;
import java.util.Vector;

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;
}

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();

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;
}
frequentRenterPoints++;
if(each.getMovie().getPriceCode() == Movie.NEW_RELEASE && each.getDaysRented() > 1){
frequentRenterPoints++;
}
result += "\t" + each.getMovie().getTitle() + "\t" +String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}

result +="Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
return result;
}
}

上面的代码有问题么?实际上是没有的,如果只是在这个店里用,并且不需要添加功能,那么这个实现是没有问题的。

但是这个世界上唯一不变的就是变化,因为随着需求的变更,我们可能需要加入新的功能

  • 比如说添加新的CD种类,那么就要修改statement()函数中的switch语句,这样就违背了“对扩展开放,对修改封闭的原则”。
  • 比如说我们需要生成新的表单格式,那么就会有另一个statement()函数,就会有大量的重复代码需要提取等等。

因此我们按照书中的方式,对上面的代码进行一步一步的重构

第一步

首先statement的函数过长,需要通过抽取局部变量与参数,然后将其提取参数。
局部变量为each和thisAmount。thisAmount在switch语句中使用,因此可以将其抽出成为函数。

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
53
54
55
56
57
58
59
60
61
import java.util.Enumeration;
import java.util.Vector;

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;
}

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);

frequentRenterPoints++;
if(each.getMovie().getPriceCode() == Movie.NEW_RELEASE && each.getDaysRented() > 1){
frequentRenterPoints++;
}
result += "\t" + each.getMovie().getTitle() + "\t" +String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}

result +="Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
return result;
}

private double amountFor(Rental each){
double thisAmount = 0;
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;
}
return thisAmount;
}
}

第二步

我们上面抽取出来的函数实际上一些命名非常糟糕,比如each, thisAmount,我们要修正它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private double amountFor(Rental aRental){
double result = 0;
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;
}
return result;
}

第三步

我们抽取出来的函数跟customer没有任何关系,它利用来自rental的信息,因此把它放在customer里面并不合适,因此将其移动到Rental类中
然后修改适配。

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 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;
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;
}
}

而customer中改为调用rental中的函数

1
thisAmount = each.getCharge();

第四步

修改了这些后,实际上thisAmount也没什么用了,可以被直接替换掉了。我们将这些临时变量直接用函数的query替换掉

1
totalAmount += each.getCharge();

来替换掉

1
2
3
thisAmount = amountFor(each);
......
totalAmount += thisAmount;

第五步

计算常客积分这部分代码实际上也跟customer没多大关系,这部分也是跟Rental有关,因此将其提炼出函数,然后放到Rental中

1
2
3
4
5
6
int getFrequentRenterPoints(){
if(getMovie().getPriceCode() == Movie.NEW_RELEASE && getDaysRented() > 1){
return 2;
}
return 1;
}

这样customer类就可以修改为

1
2
Rental each = (Rental) rentals.nextElement();
result += each.getFrequentRenterPoints();

第六步

我们发现还是有两个临时变量,这里要将其优化掉,依据Replace Tmp with Query.
double totalAmount = 0;
int frequentRenterPoints = 0;
由于这两个变量在循环中使用,所以我们抽取的函数也要抽取循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private double getTotalCharge(){
double result = 0;
Enumeration rentals = _rentals.elements();
while(rentals.hasMoreElements()){
Rental each = (Rental) rentals.nextElement();
result += each.getCharge();
}
return result;
}
private double getTotalFrenquentRenterPoints(){
double result = 0;
Enumeration rentals = _rentals.elements();
while(rentals.hasMoreElements()){
Rental each = (Rental) rentals.nextElement();
result += each.getFrequentRenterPoints();
}
return result;
}

因此state函数可以简化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public String statement(){
Enumeration rentals = _rentals.elements();
String result ="Rental Record for " + getName() + "\n";
while(rentals.hasMoreElements()){
Rental each = (Rental) rentals.nextElement();

result += "\t" + each.getMovie().getTitle() + "\t" +String.valueOf(each.getCharge()) + "\n";
}

result +="Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(getTotalFrenquentRenterPoints()) + " frequent renter points";
return result;
}

第七步

经过上面的重构,当我们再添加一个功能的时候,就很简单了,而且如果要修改一个计费逻辑的话,就只需要修改一处代码即可。
htmlStatement

1
2
3
4
5
6
7
8
9
10
11
12
13
public String htmlStatement(){
Enumeration rentals = _rentals.elements();
String result ="<H1>Rental Record for<EM>" + getName() + "</EM></H1><P>\n";
while(rentals.hasMoreElements()){
Rental each = (Rental) rentals.nextElement();

result += "\t" + each.getMovie().getTitle() + "\t" +String.valueOf(each.getCharge()) + "</BR>\n";
}

result +="<P>Amount owed is <EM>" + String.valueOf(getTotalCharge()) + "</EM></P>\n";
result += "You earned <EM>" + String.valueOf(getTotalFrenquentRenterPoints()) + "</EM> frequent renter points<P>";
return result;
}

第八步

下面要处理的是switch语句,一般来说不要在另一个对象的属性基础上使用switch语句,即使不得不使用的场景下,也要在对象自己的数据属性上使用。
我们的switch在Rental类里面,但是用到了movie的属性数据,这暗示我们要将getCharge移动到Movie里面。因此这一步我们就来处理这个问题

同理这里面getFrequentRenterPoints也用到了Movie类的数据,一样的做一下搬迁

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
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 setProceCode(int arg){
_priceCode = arg;
}
public String getTitle(){
return _title;
}
double getCharge(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;
}
return 1;
}
}

这里rental类就变得很简单了。直接调用movie中的getCharge和getFrequentRenterPoints方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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(){
return _movie.getCharge(_daysRented);
}

int getFrequentRenterPoints(){
return _movie.getFrequentRenterPoints(_daysRented);
}
}

第九步

下面可以将不同的电影类型作为子类来分别处理,这样每个类型就有自己的计费方法了,这样就能解决掉switch语句了。但是这样也有个问题,就是每个movie实际上在自己的生命周期内是可以改变自己的类型的,但是对象却不可以改变类,这就尴尬了,因此需要用到state设计模式。

首先我们用一个函数setPriceCode(priceCode)来代替_priceCode,然后新创建一个price类,这样就解决了上面的问题,用状态解决了问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract public class Price {
abstract int getPriceCode();
}

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

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

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

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
53
54
55
56
57
58
59
60
61
62
import sun.awt.util.IdentityLinkedList;

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 Price _price;

public Movie(String title, int priceCode){
_title = title;
setPriceCode(priceCode);
}
public int getPriceCode(){
return _price.getPriceCode();
}
public void setPriceCode(int arg){
switch (arg){
case REGULAR:
_price = new RegularPrice();
break;
case NEW_RELEASE:
_price = new ChildrenPrice();
break;
case CHILDRENS:
_price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
public String getTitle(){
return _title;
}
double getCharge(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;
}
return 1;
}
}

第十步

下面我们将getCharge搬迁到Price类里面去。

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
abstract public class Price {
abstract int getPriceCode();

double getCharge(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 ChildrenPrice extends Price{
int getPriceCode(){
return Movie.CHILDRENS;
}
}

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

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

Movie类中如下:

1
2
3
double getCharge(int dayRented){
return _price.getCharge(dayRented);
}

第十一步

下面就是拆分getCharge到各个子类中。这样也就分解了switch,然后将父类的函数声明为abstract

同理也要拆分getFrequentRenterPoints

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
abstract public class Price {
abstract int getPriceCode();

abstract double getCharge(int dayRented);

int getFrequentRenterPoints(int dayRented){
return 1;
}
}

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

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

class NewReleasePrice extends Price{
int getPriceCode(){
return Movie.NEW_RELEASE;
}
double getCharge(int dayRented){
return dayRented * 3;
}

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

class RegularPrice extends Price{
int getPriceCode(){
return Movie.REGULAR;
}
double getCharge(int dayRented){
double result = 2;
if(dayRented > 2) {
result += (dayRented -2) * 1.5;
}
return result;
}
}

总结

以上就是重构例子中所有的修改,通过这些修改,当我们无论修改价格还是积分或者影片类型等的时候,都可以很简单的进行修改,不会影响到其他的类。

显示 Gitment 评论