1.4 运用多态取代与价格相关的条件逻辑
1.4.1 getCharge()的再加工
回顾一下这个方法:
//Rental类中
/**
* 获取收费的金额
*
* @author newre
* @return
*/
public double getCharge() {
double result = 0;
switch (this.getMovie().getPriceCode()) {
case Movie.REGULAR :
result += 2;
if (this.getDayRented() > 2)
result += (this.getDayRented() - 2) * 1.5;
break;
case Movie.NEWRELEASE :
result += this.getDayRented() * 3;
break;
case Movie.CHILDRENS :
result += 1.5;
if (this.getDayRented() > 3)
result += (this.getDayRented() - 3) * 1.5;
break;
default :
break;
}
return result;
}
我们仔细想想搬移函数的法则:
当A类中某部分代码与B类交往密切时,就需要考虑把这部分代码搬移到B类中去。
问题就出现在switch语句上。最好不要在另一个对象的属性基础上使用switch语句。如果不得不使用,也应该在对象自身数据上使用,而非寄居在别人那里。
重构需要解耦,哪怕方法变得越来越多也在所不惜。
在此时,getCharge()方法的主体又要发生移动——从Rental移动到Movie类中。在充分理解之前内容的前提下,这一步易如反掌。
//Movie类
/**
* 根据天数及影片类型计算价格
*
* @author newre
* @param dayRented
* @return
*/
public double getCharge(int dayRented) {
double result = 0;
switch (this.getPriceCode()) {
case Movie.REGULAR :
result += 2;
if (dayRented > 2)
result += (dayRented - 2) * 1.5;
break;
case Movie.NEWRELEASE :
result += dayRented * 3;
break;
case Movie.CHILDRENS :
result += 1.5;
if (dayRented > 3)
result += (dayRented - 3) * 1.5;
break;
default :
break;
}
return result;
}
//Rental类
public double getCharge() {
return this.getMovie()
.getCharge(this.getDayRented());
}
方法中的result变量其实是有些碍眼的,想办法去掉(这里我用的是三元,也可以用if的形式,但是原书上没有进行这一步简化):
public double getCharge(int dayRented) {
switch (this.getPriceCode()) {
case Movie.REGULAR :
return dayRented > 2 ? 2 + (dayRented - 2) * 1.5 : 2;
case Movie.NEWRELEASE :
return dayRented * 3;
case Movie.CHILDRENS :
return dayRented > 3 ? 1.5 + (dayRented - 3) * 1.5 : 1.5;
default:
return 0;
}
}
再观察Rental类中的getFrequentRenterPoint()方法,发现里面也有Movie对象的成分。
此处需要说明:之所以非要把Movie的类型都集中到本类中,是为了更好地满足‘可能会更改影片类型’这个需求。
//Movie类
/**
* 获取常客积分
*
* @author newre
* @param dayRented
* @return
*/
public int getFrequentRenterPoint(int dayRented) {
if ((this.getPriceCode() == Movie.NEWRELEASE) && dayRented > 1)
return 2;
else
return 1;
}
//Rental类
public int getFrequentRenterPoint() {
return this.getMovie()
.getFrequentRenterPoint(this.getDayRented());
}
再看看到目前为止,我们的类图变成了什么样:
/'在线作图(UML)网址:
http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
如果要修改的的话,打开网址后,直接复制上图片链接(或者粘贴下方代码)修改即可'/
@startuml
Title "影片出租店程序"
class Movie{
- private int priceCode
+ public getCharge(int dayRented)
+ public getFrequentRenterPoint(int dayRented)
}
class Rental{
- private int daysRented
+ public getCharge()
+ public getFrequentRenterPoint()
}
class Customer{
+ public statement()
+ private getTotal()
+ private getTotalFrequentRenterPoints()
}
Rental --> Movie
Customer --> Rental
@enduml
1.4.2 getCharge()的继承处理
父类是Movie,子类是不同类型的Movie,子类继承父类的getCharge()方法,这是多态的一种应用。
看一下类图:
/'在线作图(UML)网址:
http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
如果要修改的的话,打开网址后,直接复制上图片链接(或者粘贴下方代码)修改即可'/
@startuml
Title "影片父与子"
class Movie{
- private int priceCode
+ public getCharge()
+ public getFrequentRenterPoint()
}
class RegularMovie{
+ public getCharge()
+ public getFrequentRenterPoint()
}
class ChildrensMovie{
+ public getCharge()
+ public getFrequentRenterPoint()
}
class NewReleaseMovie{
+ public getCharge()
+ public getFrequentRenterPoint()
}
Movie <|-- RegularMovie
Movie <|-- ChildrensMovie
Movie <|-- NewReleaseMovie
@enduml
这里需要引入一个概念:理论与需求不能混为一谈。
的确,按照这样的设计是不错,能够省略掉switch语句,也能尽可能降低耦合,带给程序可扩展性。
但是,在本例中,电影其实是可以修改其类型的,也就是说,在生成对象后,我们应该让它保留有更改类别的能力。(但如果采用这种多态的形式,会很尴尬地发现,不同类别是截然不同的对象,不能相互转换,无法满足需求)
在这里有一种比较巧妙的解决方法:利用设计模式中的状态模式。
状态模式:当一个对象的行为取决于它的状态,并且该对象可能会在运行时刻中更改状态,就可以考虑使用状态模式。
本模式的实质在于将变化的状态抽象出来,以改变子类实例来达到目的。
拟定使用状态模式后的类图:
/'在线作图(UML)网址:
http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
如果要修改的的话,打开网址后,直接复制上图片链接(或者粘贴下方代码)修改即可'/
@startuml
Title "状态模式"
class Movie{
+ public getCharge()
}
abstract class Price{
+ public getCharge()
}
class RegularPrice{
+ public getCharge()
}
class ChildrensPrice{
+ public getCharge()
}
class NewReleasePrice{
+ public getCharge()
}
Movie o-- Price
Price <|-- RegularPrice
Price <|-- ChildrensPrice
Price <|-- NewReleasePrice
@enduml
如果你很熟悉GoF所列的各种模式,可能会问:“这是一个状态,还是一个策略?”答案取决于Price类究竟代表计费方式,还是代表影片的某个状态。在这个阶段,对于模式(和其名称)的选择反映出你对结构的想法。此刻我把它视为影片的某种状态。如果未来我觉得策略能更好地说明我的意图,我会再重构它,修改名字,以形成策略。
这仅仅是提出了一个结构,要把原程序加工成这个样子,还需要继续重构,下面将分为三个步骤进行。
-
状态/策略取代类型码(8.15 Replace Type Code with State/Strategy)
-
搬移函数(7.1 Move Method)
-
以多态取代条件表达式(9.6 Replace Conditional with Polymorphism)