重构,改善既有的代码设计

以下将以6次重构的操作来实现一个简单的案例。

例子:这是一个影片出租店用的程序,计算每一个顾客的消费金额并打印详单。操作者告诉程序:顾客租了哪些影片,租期多长,程序便根据租赁时间和影片的类型算出费用。影片分为三类:普通片,儿童片和新片。除了计算费用,还要为常客计算积分,积分会根据租片的种类是否为新片而有所不同。


  • 首先提个问题:什么时候重构?
  • 看代码实现上面案例如下:

    Movie.java(影片实体类)

    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 class Movie {

    public static final int CHILEDRENS = 2;
    public static final int REGULAR = 0;
    public static final int NEW_RELEASE = 1;


    public Movie(String title, int priceCode) {
    this.title = title;
    this.priceCode = priceCode;
    }

    private String title;
    private int priceCode;

    public String getTitle() {
    return title;
    }

    public void setTitle(String title) {
    this.title = title;
    }

    public int getPriceCode() {
    return priceCode;
    }

    public void setPriceCode(int priceCode) {
    this.priceCode = priceCode;
    }
    }

Rental.java(租赁类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Rental {

private Movie movie;
private int daysRented;

public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}

public int getDaysRented() {
return daysRented;
}

public Movie getMovie() {
return movie;
}
}

Customer.java(顾客租赁类)

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
public class Customer {

private String name;
private Vector rentals = new Vector();

public void addRental(Rental arg) {
rentals.addElement(arg);
}

public String getName() {
return name;
}

public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration elements = rentals.elements();
StringBuilder result = new StringBuilder("Rental Record for " + getName() + "\n");
while (elements.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) elements.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.CHILEDRENS:
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.append("\t").append(each.getMovie().getTitle()).append("\t").append(String.valueOf(thisAmount)).append("\n");
totalAmount += thisAmount;
}
//add footer lines
result.append("Amount owed is ").append(String.valueOf(totalAmount)).append("\n");
result.append("You earned ").append(String.valueOf(frequentRenterPoints)).append(" frequent renter points");
return result.toString();
}
}
以上代码实现完全没有问题,但是能如果遇到以下这两个问题,就会突出臃肿,可扩展性差,难以维护等缺点了。
  • 如果计费标准变化了?
    • 就需要找到每条case中的计费方式,整个switch语句会十分庞大。
  • 如果想输出statement()中某个数据显示到表单或者被其他地方引用?
    • 没有任何方法独立提出来,导致重复引用的计算多处出现。

如果你发现自己需要为程序添加一个特性,而代码的结构使你无法很方便地达到目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。


###重构(一)

分解并重组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
46
47
48
49
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration elements = rentals.elements();
StringBuilder result = new StringBuilder("Rental Record for " + getName() + "\n");
while (elements.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) elements.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.append("\t").append(each.getMovie().getTitle()).append("\t").append(String.valueOf(thisAmount)).append("\n");
totalAmount += thisAmount;
}
//add footer lines
result.append("Amount owed is ").append(String.valueOf(totalAmount)).append("\n");
result.append("You earned ").append(String.valueOf(frequentRenterPoints)).append(" frequent renter points");
return result.toString();
}

private int amountFor(Rental each){
int 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.CHILEDRENS:
thisAmount += 1.5;
if (each.getDaysRented() > 3) {
thisAmount += (each.getDaysRented() - 3) * 1.5;
}
break;
}
return thisAmount;
}

每次做完一次这样的修改都要编译并测试。这次测试发现错误了吧。故意把返回的double改成了int类型。
就是为了告诉我们每次修改都要非常小心,并且编译测试,不但不会浪费时间,反而会节省大量的调试时间。

###重构(二)

修改函数名称/参数名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private double amountFor(Rental rental){
double result = 0;
switch (rental.getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (rental.getDaysRented() > 2) {
result += (rental.getDaysRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
result += rental.getDaysRented() * 3;
break;
case Movie.CHILEDRENS:
result += 1.5;
if (rental.getDaysRented() > 3) {
result += (rental.getDaysRented() - 3) * 1.5;
}
break;
}
return result;
}

任何一个傻瓜都可以写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。

###重构(三)

搬移函数(移动到它该到的地方去)
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 class Customer {

private String name;
private Vector rentals = new Vector();

public void addRental(Rental arg) {
rentals.addElement(arg);
}

public String getName() {
return name;
}

public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration elements = rentals.elements();
StringBuilder result = new StringBuilder("Rental Record for " + getName() + "\n");
while (elements.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) elements.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.append("\t").append(each.getMovie().getTitle()).append("\t").append(String.valueOf(thisAmount)).append("\n");
totalAmount += thisAmount;
}
//add footer lines
result.append("Amount owed is ").append(String.valueOf(totalAmount)).append("\n");
result.append("You earned ").append(String.valueOf(frequentRenterPoints)).append(" frequent renter points");
return result.toString();
}

private double amountFor(Rental rental){
return rental.getCharge();
}
}

Rental.java类

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
public class Rental {

private Movie movie;
private int daysRented;

public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.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.CHILEDRENS:
result += 1.5;
if (getDaysRented() > 3) {
result += (getDaysRented() - 3) * 1.5;
}
break;
}
return result;
}
}

重构(四)

提炼函数(越细小的函数功能越明确)/去除临时变量(使结构更清晰)
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 elements = rentals.elements();
StringBuilder result = new StringBuilder("Rental Record for " + getName() + "\n");
while (elements.hasMoreElements()) {
Rental each = (Rental) elements.nextElement();

//show figures for this rental
result.append("\t").append(each.getMovie().getTitle()).append("\t").append(String.valueOf(each.getCharge())).append("\n");
}
//add footer lines
result.append("Amount owed is ").append(String.valueOf(getTotalCharge())).append("\n");
result.append("You earned ").append(String.valueOf(getTotalFrequentRenterPoints())).append(" frequent renter points");
return result.toString();
}

private double getTotalCharge(){
double result = 0;
Enumeration enumeration = rentals.elements();
while (enumeration.hasMoreElements()){
Rental each = (Rental) enumeration.nextElement();
result += each.getCharge();
}
return result;
}

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

Rental.java类

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 Rental {

private Movie movie;
private int daysRented;

public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.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.CHILEDRENS:
result += 1.5;
if (getDaysRented() > 3) {
result += (getDaysRented() - 3) * 1.5;
}
break;
}
return result;
}

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

###重构(五)

多态来提炼switch()语句
  • 这里不直接抽象Movie类,是因为Movie类有自己的属性和生命周期。所以State模式的应用可以很好的解决这个问题。

    image

Movie.java类

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
public class Movie {

public static final int CHILEDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;

public Movie(String title, int priceCode) {
this.title = title;
setPriceCode(priceCode);
}

public void setPriceCode(int arg) {
switch (arg) {
case REGULAR:
price = new RegularPrice();
break;
case CHILEDRENS:
price = new ChildrensPrice();
break;
case NEW_RELEASE:
price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("incorrect price code");
}
}

private String title;
private Price price;

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public Price getPrice() {
return price;
}

public void setPrice(Price price) {
this.price = price;
}

double getCharge(int daysRented) {
return price.getCharge(daysRented);
}

int getFrequentRenterPoints(int daysRented) {
return price.getFrequentRenterPoints(daysRented);
}
}

Price.java类

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
abstract class Price {

abstract int getPriceCode();

double getCharge(int daysRented){
double result = 0;
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.CHILEDRENS:
result += 1.5;
if (daysRented > 3) {
result += (daysRented - 3) * 1.5;
}
break;
}
return result;
}

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

NewReleasePrice.java类

1
2
3
4
5
6
public class NewReleasePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
}

其他两个实现类同NewReleasePrice类,就不列出来了。

###重构(六)

继承
1
2
3
4
5
6
7
8
9
10
abstract class Price {

abstract int getPriceCode();

abstract double getCharge(int daysRented);

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

NewReleasePrice.java类

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

前面列出的技术点仅仅只是一个起点,使你登堂入室之前的大门。如果没有这些技术,你根本无法对运行的程序进行任何设计上的改动。有了这些技术,你仍然做不到,但起码可以开始尝试了。