重构:改善既有代码的设计(2)

文章说明:

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 MethodReplace Conditional with Polymorpbism所有这些重构行为的目的都是为了是责任分配更合理,代码维护更容易。它将与结构化的编程方式有很大区别,尽管很多人习惯后者。不过只要你一习惯重构后的风格,你就很难在回到过去了,因为结构化的编程风格已经不能满足你的需求了。

  这个实例给你上的最重要一课:重构的节奏,测试,小修改,测试,小修改,测试,小修改…这是这样的节奏让重构既快速又安全。如果你能跟上这个节奏,你现在应该对重构有一个基本了解了,后面我们将了解一点背景,原理和理论,当然只是一点点。

posted on 2011-09-12 23:03 luecsc 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/luecsc/archive/2011/09/12/2174265.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值