一個旅館提供各種飲料(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的例子,比如讀寫文件:
