本节书摘来自异步社区《重构:改善既有代码的设计》一书中的第1章,第1.4节运用多态取代与价格相关的条件逻辑,作者【美】Martin Fowler,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.4 运用多态取代与价格相关的条件逻辑
这个问题的第一部分是switch语句。最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。
class Rental...
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类里去:
class Movie...
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对象。计算费用时需要两项数据:租期长度和影片类型。为什么我选择将租期长度传给Movie对象,而不是将影片类型传给Rental对象呢?因为本系统可能发生的变化是加入新影片类型,这种变化带有不稳定倾向。如果影片类型有所变化,我希望尽量控制它造成的影响,所以选择在Movie对象内计算费用。
我把上述计费方法放进Movie类,然后修改Rental的getCharge(),让它使用这个新函数(图1-12和图1-13):
class Rental...
double getCharge() {
return _movie.getCharge(_daysRented);
}
搬移getCharge()之后,我以相同手法处理常客积分计算。这样我就把根据影片类型而变化的所有东西,都放到了影片类型所属的类中。以下是重构前的代码:
class Rental...
int getFrequentRenterPoints() {
if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1)
return 2;
else
return 1;
}
重构后的代码如下:
class Rental...
int getFrequentRenterPoints() {
return _movie.getFrequentRenterPoints(_daysRented);
}
class Movie...
int getFrequentRenterPoints(int daysRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
return 2;
else
return 1;
}
终于……我们来到继承
我们有数种影片类型,它们以不同的方式回答相同的问题。这听起来很像子类的工作。我们可以建立Movie的三个子类,每个都有自己的计费法(图1-14)。
这么一来,我就可以用多态来取代switch语句了。很遗憾的是这里有个小问题,不能这么干。一部影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。不过还是有一个解决方法:State模式[Gang of Four]。运用它之后,我们的类看起来像图1-15。
加入这一层间接性,我们就可以在Price对象内进行子类化动作[4],于是便可在任何必要时刻修改价格。
如果你很熟悉GoF(Gang of Four,四巨头)[5]所列的各种模式,可能会问:“这是一个State,还是一个Strategy?”答案取决于Price类究竟代表计费方式(此时我喜欢把它叫做Pricer还PricingStrategy),还是代表影片的某个状态(例如“Star Trek X是一部新片”)。在这个阶段,对于模式(和其名称)的选择反映出你对结构的想法。此刻我把它视为影片的某种状态。如果未来我觉得Strategy能更好地说明我的意图,我会再重构它,修改名字,以形成Strategy。
为了引入State模式,我使用三个重构手法。首先运用Replace Type Code with State/Strategy (227),将与类型相关的行为搬移至State模式内。然后运用Move Method (142)将switch语句移到Price类。最后运用Replace Conditional with Polymorphism (255)去掉switch语句。
首先我要使用Replace Type Code with State/Strategy (227)。第一步骤是针对类型代码使用Self Encapsulate Field (171),确保任何时候都通过取值函数和设值函数来访问类型代码。多数访问操作来自其他类,它们已经在使用取值函数。但构造函数仍然直接访问价格代码[6]:
class Movie...
public Movie(String title, int priceCode) {
_title= title;
_priceCode = priceCode;
}
我可以用一个设值函数来代替:
class Movie
public Movie(String title, int priceCode) {
_title = title;
setPriceCode(priceCode);
}
然后编译并测试,确保没有破坏任何东西。现在我新建一个Price类,并在其中提供类型相关的行为。为了实现这一点,我在Price类内加入一个抽象函数,并在所有子类中加上对应的具体函数:
abstract class Price {
abstract int getPriceCode();
}
class ChildrensPrice 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类内的“价格代号”访问函数(取值函数/设值函数,如下),让它们使用新类。下面是重构前的样子:
public int getPriceCode() {
return _priceCode;
}
public setPriceCode(int arg) {
_priceCode = arg;
}
private int _priceCode;
这意味着我必须在Movie类内保存一个Price对象,而不再是保存一个_priceCode变量。此外我还需要修改访问函数:
class Movie...
public int getPriceCode() {
return _price.getPriceCode();
}
public void setPriceCode(int arg) {
switch (arg) {
case REGULAR:
_price = new RegularPrice();
break;
case CHILDRENS:
_price = new ChildrensPrice();
break;
case NEW_RELEASE:
_price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
private Price _price;
现在我可以重新编译并测试,那些比较复杂的函数根本不知道世界已经变了个样儿。
现在我要对getCharge()实施Move Method (142)。下面是重构前的代码:
class Movie...
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;
}
搬移动作很简单。下面是重构后的代码:
class Movie...
double getCharge(int daysRented) {
return _price.getCharge(daysRented);
}
class Price...
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;
}
搬移之后,我就可以开始运用Replace Conditional with Polymorphism (255)了。
下面是重构前的代码:
class Price...
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;
}
我的做法是一次取出一个case分支,在相应的类建立一个覆盖函数。先从RegularPrice开始:
class RegularPrice...
double getCharge(int daysRented) {
double result = 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
return result;
}
这个函数覆盖了父类中的case语句,而我暂时还把后者留在原处不动。现在编译并测试,然后取出下一个case分支,再编译并测试。(为了保证被执行的确实是子类中的代码,我喜欢故意丢一个错误进去,然后让它运行,让测试失败。噢,我是不是有点太偏执了?)
class ChildrensPrice
double getCharge(int daysRented) {
double result = 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
return result;
}
class NewReleasePrice...
double getCharge(int daysRented) {
return daysRented * 3;
}
处理完所有case分支之后,我就把Price.getCharge()声明为abstract:
class Price...
abstract double getCharge(int daysRented);
现在我可以运用同样手法处理getFrequentRenterPoints()。重构前的样子如下[7]:
class Movie...
int getFrequentRenterPoints(int daysRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
return 2;
else
return 1;
}
首先我把这个函数移到Price类:
class Movie...
int getFrequentRenterPoints(int daysRented) {
return _price.getFrequentRenterPoints(daysRented);
}
class Price...
int getFrequentRenterPoints(int daysRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
return 2;
else
return 1;
}
但是这一次我不把超类函数声明为abstract。我只是为新片类型增加一个覆写函数,并在超类内留下一个已定义的函数,使它成为一种默认行为。
class NewReleasePrice
int getFrequentRenterPoints(int daysRented) {
return (daysRented > 1) ? 2 : 1;
}
class Price...
int getFrequentRenterPoints(int daysRented) {
return 1;
}
引入State模式花了我不少力气,值得吗?这么做的收获是:如果我要修改任何与价格有关的行为,或是添加新的定价标准,或是加入其他取决于价格的行为,程序的修改会容易得多。这个程序的其余部分并不知道我运用了State模式。对于我目前拥有的这么几个小量行为来说,任何功能或特性上的修改也许都不合算,但如果在一个更复杂的系统中,有十多个与价格相关的函数,程序的修改难易度就会有很大的区别。以上所有修改都是小步骤进行,进度似乎太过缓慢,但是我一次都没有打开过调试器,所以整个过程实际上很快就过去了。我写本章文字所用的时间,远比修改那些代码的时间多得多。
现在我已经完成了第二个重要的重构行为。从此,修改影片分类结构,或是改变费用计算规则、改变常客积分计算规则,都容易多了。图1-16和图1-17描述State模式对于价格信息所起的作用。