《重构:改善既有代码的设计》这本书啊,几乎所有的精华都在这个书中最开始的例子里。本文就是对这个例子的总结。
任务描述
例子中是一个CD店,这个店有个管理系统,用于记录顾客的消费,积分并打印消费清单,计算费用等。是一个很简单的管理系统。那么最初的架构是这样的:
时序图如下所示:
Movie类是一个数据类,用于存放Movie的相关属性信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public 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
15public 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
55import 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
61import 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
19private 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
35public 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
3thisAmount = amountFor(each);
......
totalAmount += thisAmount;
第五步
计算常客积分这部分代码实际上也跟customer没多大关系,这部分也是跟Rental有关,因此将其提炼出函数,然后放到Rental中1
2
3
4
5
6int getFrequentRenterPoints(){
if(getMovie().getPriceCode() == Movie.NEW_RELEASE && getDaysRented() > 1){
return 2;
}
return 1;
}
这样customer类就可以修改为1
2Rental 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
18private 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
13public 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 | public String htmlStatement(){ |
第八步
下面要处理的是switch语句,一般来说不要在另一个对象的属性基础上使用switch语句,即使不得不使用的场景下,也要在对象自己的数据属性上使用。
我们的switch在Rental类里面,但是用到了movie的属性数据,这暗示我们要将getCharge移动到Movie里面。因此这一步我们就来处理这个问题
同理这里面getFrequentRenterPoints也用到了Movie类的数据,一样的做一下搬迁
1 | public class Movie { |
这里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
22public 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
21abstract 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 | import sun.awt.util.IdentityLinkedList; |
第十步
下面我们将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
42abstract 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
3double getCharge(int dayRented){
return _price.getCharge(dayRented);
}
第十一步
下面就是拆分getCharge到各个子类中。这样也就分解了switch,然后将父类的函数声明为abstract
同理也要拆分getFrequentRenterPoints1
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
48abstract 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;
}
}
总结
以上就是重构例子中所有的修改,通过这些修改,当我们无论修改价格还是积分或者影片类型等的时候,都可以很简单的进行修改,不会影响到其他的类。