文章说明:
1.本文代码为了便于对照,先给出重构前代码,然后给出重构后的代码
2.原著作中部分UML图,本文未给出
1.用多态代替价格条件逻辑代码
经过前一阶段的重构,我们注意到switch语句:在Rental类中使用了Movie类的属性,这不是什么好主意,如果不得不使用,我们应该尽量在自己对象上使用自己的数据,而不应该过多的使用别人的数据,如:
public class Rental... public 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; }
这暗示我们应该先把getCharge()移动到Movie中:
public class Movie ...
public 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.CHILDRENS:
result += 1.5;
if(daysRented>3)
result += (daysRented-3)*1.5;
break;
}
return result;
}
}
为了能够让程序正常运行,我们需要将租期长度作为参数传递进去。然而,租期长度又来自于Rental类,这里你就可能产生疑问了。既然switch语句会影响两个类中的数据,我们为什么要把getCharge()从Rental(此方法在Rental中表示:将影片类型传递到Rental对象)类中移到Movie(将租期长度传递到Movie对象)类中呢?因为本系统可能发生的变化是影片类型的改变,这种变化是不稳定的。我希望引起的连锁反应是最小的,所以选择在Movie中计算费用。相应的,应该改变Rental类:
public class Rental ...
public double getCharge() {
return movie.getCharge(daysRented);
}
移动了getCharge()方法后,我以同样的手法处理点数(积分)的计算,以保证把将会因影片类型改变而改变的代码都放入Movie类中。Rental类就由:
public class Rental ...
public int getFrequentRenterPoints() {
if((getMovie().getPriceCode()==Movie.NEW_RELEASE)&& getDaysRented()>1)
return 2;
else
return 1;
}
}
变成:
public class Rental ...
public int getFrequentRenterPoints() {
return movie.getFrequentRenterPoints(daysRented);
}
}
相应的Movie类中新增加一方法:
public class Movie...
public int getFrequentRenterPoints(int daysRented) {
if(getPriceCode()==Movie.NEW_RELEASE && daysRented>1)
return 2;
else
return 1;
}
}
2.终于说到继承了
我们有不同种类型的影片,它们以不同的类型回答相同的问题(影片类型的不同,都是为了计算出租赁的费用)。所以,我们想到了可以建立3个子类,每个子类都有自己的计费方式,它们计算各自的费用,它们的关系如下(以继承机制表现不同影片):
这样,我们就可以用多态取代switch语句了。但是遗憾的是,我们不能这么干,因为一部影片可以在自己的生命周期内修改自己的类型,一个对象却不能在生命周期类修改自己所属的类。但是,我们仍有解决的办法,那就是运用State模式(或译:状态模式),运用它以后,我们的类关系应该如下(运动State模式表现不同的影片):
这里增加了一个中间层,就可以在Price对象进行继承动作了,我们可以按照我们的需求随时改变Price(价格)。
如果你很熟悉设计模式,你可能会问:这是一个State还是一个strategy?答案取决于Price类究竟代表计费方式还是代表影片的某个状态。对于模式的选择反映出你对结构的想法,这里把它视为影片的某种状态。如果未来你觉得strategy能更好的说明你的意图,你可以再改造它以形成strategy。
接下来,我将运用3个重构准则。首先运用了Replace Type Code with State/strategy,将与影片类型相互依赖的行为(Type Code bahavior)移动到State模式内。然后运用Move Method将switch语句移到了Price类中,最后运用Replace Conditional with Polymorphism去掉switch语句。
首先我使用Replace Type Code with State/strategy。第一步将与类型相依赖的属性使用Self Encapsulate Field以确保任何时候都可以通过get和set方法获得这些属性。这样做是因为其它类中的很多代码都已经使用了这些属性,大多数方法也通过get方法来获取这些属性。当然,构造方法仍然可以直接使用属性值。
public class Movie ...
private String title; //片名
private int priceCode; //价格代号
//getter and setter
}
构造方法中我们可以用setter代替,如:
public class Movie ...
private String title; //片名
private int priceCode; //价格代号
//getter and setter
public Movie(String title, int priceCode) {
this.title = title;
setPriceCode(priceCode);
}
}
现在我加入新的类,在Price 类中提供抽象方法getPriceCode(与类型相依赖的行为),子类中来实现这个抽象方法:
public abstract class Price {
abstract int getPriceCode();
}
public class ChildrensPrice extends Price {
@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
}
public class NewReleasePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
}
public class RegularPrice extends Price {
@Override
int getPriceCode() {
return Movie.REGULAR;
}
}
先在我要修改Movie的访问方法(get和set方法),下面是重构前的样子:
private int priceCode; //价格代号
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}
这意味着,我必须在Movie类中保存一个Price对象而不再是一个priceCode变量,此外我还要修改访问方法:
public class Movie ...
private Price price;
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int arg) {
switch(arg) { //各种影片的价格不同
case REGULAR:
price = new RegularPrice();
break;
case NEW_RELEASE:
price = new NewReleasePrice();
break;
case Movie.CHILDRENS:
price = new ChildrensPrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
第二步,我将对getCharge()运用Move Method,重构之前如下:
public class Movie ...
public 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.CHILDRENS:
result += 1.5;
if(daysRented>3)
result += (daysRented-3)*1.5;
break;
}
return result;
}
重构之后:
public class Movie ...
public double getCharge(int daysRented) {
return price.getCharge(daysRented);
}
public abstract class Price ...
public 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.CHILDRENS:
result += 1.5;
if(daysRented>3)
result += (daysRented-3)*1.5;
break;
}
return result;
}
}
第3步,我将运用Replace Conditional with Polymorphism,将switch的每一个分支用于一个子类的覆写方法,重构后如下:
public abstract class Price {
abstract double getCharge(int daysRented);
}
public class RegularPrice extends Price {
public double getCharge(int daysRented) {
double result = 2;
if(daysRented>2)
result += (daysRented-2)*1.5;
return result;
}
}
public class NewReleasePrice extends Price {
@Override
public double getCharge(int daysRented) {
return daysRented*3;
}
}
public class ChildrensPrice extends Price {
@Override
public double getCharge(int daysRented) {
double result = 1.5;
if(daysRented>3)
result += (daysRented-3)*1.5;
return result;
}
}
最后,在对象点数(积分)的计算采用同样的方法重构,这里不再累赘叙述。这里需要注意的是普通片和儿童片的积分点数是1,新片的积分点数是2,重构的方法看下面的UML图就容易理解了:
引入State模式花费了我们不少功夫,这样做是否值得呢?如果我要修改与任何与价格有关的行为,增加一个新的价格标准,或者其它有关价格的行为,我都将很容易的对系统进行修改,而这个程序的其它部分并不知道我运用了State模式。由于目前程序中的行为太少,所以我们修改起来很容易,当在一个大的系统中,比如与价格相关的行为有十多个,修改的难度与这个相比将会有很大区别。
3.结语
这是一个简单的实例,我希望它能够让你对重构有一点感觉。实例中我使用了几个重构准则:Extract Method,Move Method和Replace Conditional with Polymorpbism。所有这些重构行为的目的都是为了是责任分配更合理,代码维护更容易。它将与结构化的编程方式有很大区别,尽管很多人习惯后者。不过只要你一习惯重构后的风格,你就很难在回到过去了,因为结构化的编程风格已经不能满足你的需求了。
这个实例给你上的最重要一课:重构的节奏,测试,小修改,测试,小修改,测试,小修改…这是这样的节奏让重构既快速又安全。如果你能跟上这个节奏,你现在应该对重构有一个基本了解了,后面我们将了解一点背景,原理和理论,当然只是一点点。