一个旅馆提供各种饮料(Beverage), 比如有HouseBlend,DarkRoast等,每个饮料还有很多配料比如Milk,Soy等。如何写一个程序能够方便的输出每种饮料的价格呢?(包括饮料+配料)
最笨的方法如下:也就是每种饮料,每种饮料加配料的组合,都写一个类。
package javaapplication33;
public class Main {
public static void main(String[] args) {
Beverage b=new HouseBlendWithMilk();
System.out.println(b.getDescription());
System.out.println(b.cost());
}
}
abstract class Beverage {
public abstract String getDescription();
public abstract int cost();
}
class HouseBlend extends Beverage {
@Override
public String getDescription() {
return "A cup of HouseBlend";
}
@Override
public int cost() {
return 10;
}
}
class DarkRoast extends Beverage {
@Override
public String getDescription() {
return "A cup of DarkRoast";
}
@Override
public int cost() {
return 8;
}
}
class HouseBlendWithMilk extends Beverage{
@Override
public String getDescription() {
return "A cup of HouseBlend with a soap of milk.";
}
@Override
public int cost() {
return 12;
}
}
问题来了,如果饮料及其配料的排列组合有20种,30种,这很可能。那么是不是就要继承Beverage写上20,30个类呢?这倒不是关键,比如万一Milk的价格变动了,那么所有有关Milk的类都要把cost()方法重写。。。。这才恐怖呢。原因是我们没有把变化的部分封装起来。
OK,我们开始封装。为方便起见,我们仍把主要的饮料HouseBlend, DarkRoast等写成父类Beverage的子类。我们把造成变化的配料Milk,Soy等把它们封装起来,封装到哪?封装到父类里面去。看看下面这样行不行:
package javaapplication33;
public class Main {
public static void main(String[] args) {
Beverage b = new HouseBlend();
b.setMilk(true); //增加一个配料
System.out.println(b.cost());
b.setSoy(true);
System.out.println(b.getDescription());
System.out.println(b.cost());
b.setMilk(false);
System.out.println(b.cost());
b.setSoy(false);
System.out.println(b.cost());
System.out.println(b.getDescription());
}
}
abstract class Beverage {
int MILKPRICE = 2;
int SOYPRICE = 3;
public boolean milk;
public boolean soy;
int cost;
String description;
public void setMilk(boolean b) {
milk = b;
}
public void setSoy(boolean b) {
soy = b;
}
public int cost() {
cost = 0;
if (milk) {
cost += MILKPRICE;
}
if (soy) {
cost += SOYPRICE;
}
return cost;
}
public String getDescription() {
description = "";
if (milk) {
description += " with a soap of milk";
}
if (soy) {
description += " with a bottle of soy";
}
return description;
}
}
class HouseBlend extends Beverage {
@Override
public String getDescription() {
return "a cup of HouseBlend" + super.getDescription();
}
@Override
public int cost() {
return super.cost() + 10;
}
}
class DarkRoast extends Beverage {
@Override
public String getDescription() {
return "a cup of DarkRoast" + super.getDescription();
}
@Override
public int cost() {
return super.cost() + 8;
}
}
这样,每当我需要一个饮料加几个配料时,仅需要new这个饮料,再set配料即可。貌似这个程序没问题了。
貌似!
(1) 当配料的价格改动时,Beverage这个类需要改动。设计模式的原则是在提供扩展的同时,尽可能不改动原有的程序的。
(2) 如果新增,或者减少某几种配料,同理,Beverage仍需改动。
(3) 如果我们要双份的Milk呢?
崩溃!
以上,第一种思路是把所有的排列组合都写成子类,当一个配料变动,会导致所有相关的组合都要变动。
第二种思路是把饮料写成子类,配料的设置封装到父类中去,当配料变动时,会导致原有的父类变动,并且无法同时提供两份同样的配料。
其实当第一种思路被否定掉的时候就有一种冲动,就是把饮料和配料都写成子类,继承抽象的父类Beverage。这个其实很容易做到。只是主程序在调用时,比如我要一个HouseBlend配上Milk的时候,我该怎么生成呢?new一个HouseBlend,再new一个Milk,然后呢?怎么让它打印出“A Houseblend with a cup of milk”这样,并且算价格的时候直接两者的cost就能叠加呢?
我们想到了在读写文件操作时的样子: new XXX(new XXX(new XXXXX())); 如果饮料加上配料可以这样生成,岂不是全都解决了?
package javaapplication32;
public class Main {
public static void main(String[] args) {
Baverage b = new Milk(new HouseBland(new Soy()));
System.out.println(b.getDescription());
System.out.println(b.getCost());
}
}
abstract class Baverage {
abstract String getDescription();
abstract int getCost();
}
class HouseBland extends Baverage {
Baverage baverage;
HouseBland() {
}
HouseBland(Baverage baverage) {
this.baverage = baverage;
}
@Override
String getDescription() {
if (baverage != null) {
return "A cup of HouseBland" + baverage.getDescription();
}
else {
return "A cup of HouseBland";
}
}
@Override
int getCost() {
if (baverage != null) {
return 10 + baverage.getCost();
}
else {
return 10;
}
}
}
class Milk extends Baverage {
Baverage baverage;
Milk() {
}
Milk(Baverage baverage) {
this.baverage = baverage;
}
@Override
String getDescription() {
if (baverage != null) {
return baverage.getDescription() + " And a soap Milk";
}
else {
return " And a soap Milk";
}
}
@Override
int getCost() {
if (baverage != null) {
return baverage.getCost() + 5;
}
else {
return 5;
}
}
}
class Soy extends Baverage {
Baverage baverage;
Soy() {
}
Soy(Baverage baverage) {
this.baverage = baverage;
}
@Override
String getDescription() {
if (baverage != null) {
return baverage.getDescription() + " And a bottle of Soy";
}
else {
return " And a bottle of Soy";
}
}
@Override
int getCost() {
if (baverage != null) {
return baverage.getCost() + 7;
}
else {
return 7;
}
}
}
问题解决了?可不可以更完美呢?我们发现每一个子类里面都用了大量的判断语句
if (baverage != null) {…..} else {…..}
几乎都是一样的,为什么每个都要写呢?原因是我们没有人为的规定Baverage b = new Milk(new HouseBland(new Soy()));这个语句的顺序。是“配料(new 饮料())” 还是“饮料(new 配料)”?如果我们规定一下顺序的话,就有很多前面的if判断语句不需要反复写。
这个改进看起来好像不重要,但是,在实际工作中,像HouseBlend这样的类就像是原先写好的类,或者几乎长时间不会改变的类;而想配料Milk这样的类,就如同经常改动或者新增进去的类。也就是说前者不怎么改动,而后者会经常变。那么我们就要尽量保证前者的稳定和简单(越简单越不易出错)。
下面的程序,我们删除了HouseBlend中的繁杂的判断语句,在主函数生成饮料加配料时,我们要保证“new 配料(new 配料(….(new 饮料())))的模式(先配料后饮料)。(因为一个饮料可以配多个配料而一个配料不可以配多个饮料)
源代码如下:
package javaapplication32;
public class Main {
public static void main(String[] args) {
Baverage b = new Milk(new Soy(new HouseBlend()));
System.out.println(b.getDescription());
System.out.println(b.getCost());
}
}
abstract class Baverage {
abstract String getDescription();
abstract int getCost();
}
class HouseBlend extends Baverage {
@Override
String getDescription() {
return "A cup of HouseBland";
}
@Override
int getCost() {
return 10;
}
}
class Milk extends Baverage {
Baverage baverage;
Milk() {
}
Milk(Baverage baverage) {
this.baverage = baverage;
}
@Override
String getDescription() {
if (baverage != null) {
return baverage.getDescription() + " And a soap Milk";
}
else {
return " And a soap Milk";
}
}
@Override
int getCost() {
if (baverage != null) {
return baverage.getCost() + 5;
}
else {
return 5;
}
}
}
class Soy extends Baverage {
Baverage baverage;
Soy() {
}
Soy(Baverage baverage) {
this.baverage = baverage;
}
@Override
String getDescription() {
if (baverage != null) {
return baverage.getDescription() + " And a bottle of Soy";
}
else {
return " And a bottle of Soy";
}
}
@Override
int getCost() {
if (baverage != null) {
return baverage.getCost() + 7;
}
else {
return 7;
}
}
}
其实我们还没揭示Decorator pattern的核心含义:配料跟主料其实不是一回事,但是为了实现主料的新功能,又要保持主料的稳定,我们就用配料继承主料,或者继承主料的父函数,目的就是获得相通的类型(type match)。
这样,我们可以在其他程序调用时,随时给主料添加配料的新功能。这就是composition组合的魅力!
最后,我们对上面的程序做一下最终的改进,原因是由于在类的层次设计的时候,我们没有区分饮料和配料之间的关系,因为它们都平行的继承了同一个抽象类。在Decorator pattern里面,往往会出现这样的情况:就是饮料的种类很稳定,而配料的种类却很繁杂。为了让程序看上去更清晰(一眼就能看出谁是主,谁是配),我们用特定的一个抽象类继承原先的父类,再让所有的配料继承该抽象类。
package javaapplication32;
public class Main {
public static void main(String[] args) {
Baverage b = new Milk(new Soy(new HouseBlend()));
System.out.println(b.getDescription());
System.out.println(b.getCost());
}
}
abstract class Baverage {
abstract String getDescription();
abstract int getCost();
}
class HouseBlend extends Baverage {
@Override
String getDescription() {
return "A cup of HouseBland";
}
@Override
int getCost() {
return 10;
}
}
abstract class Decorator extends Baverage {
abstract String getDescription();
abstract int getCost();
}
class Milk extends Decorator {
Baverage baverage;
Milk() {
}
Milk(Baverage baverage) {
this.baverage = baverage;
}
@Override
String getDescription() {
if (baverage != null) {
return baverage.getDescription() + " And a soap Milk";
}
else {
return " And a soap Milk";
}
}
@Override
int getCost() {
if (baverage != null) {
return baverage.getCost() + 5;
}
else {
return 5;
}
}
}
class Soy extends Decorator {
Baverage baverage;
Soy() {
}
Soy(Baverage baverage) {
this.baverage = baverage;
}
@Override
String getDescription() {
if (baverage != null) {
return baverage.getDescription() + " And a bottle of Soy";
}
else {
return " And a bottle of Soy";
}
}
@Override
int getCost() {
if (baverage != null) {
return baverage.getCost() + 7;
}
else {
return 7;
}
}
}
其实java API里面有很多使用Decorator Pattern的例子,比如读写文件: