前面介紹了兩篇設(shè)計模式,策略模式和觀察者模式,其實自己也是在學(xué)習(xí)階段,感覺收益很大。所以想繼續(xù)分享,把java中的23中設(shè)計模式都總結(jié)一遍,在以后才能在實踐中靈活運用。感興趣的童鞋可以看看前面分享的兩篇:
前面兩篇都是上來就是例子,需求,我想改變一下套路,今天先介紹裝飾器的理論結(jié)構(gòu),再說例子。還是要再聲明:例子來自于《HeadFirst 設(shè)計模式》,推薦大家看看原書,寫得很淺顯易懂。
理論知識

實際問題
今天的例子是一個 coffe 的例子,相信大家都去星巴克喝過咖啡,或者奶茶,我們在買coffe的時候,首先選擇一個基本的coffe類型,比如 卡布奇洛,然后添加各種佐料:摩卡,豆?jié){,蒸奶等。基本類型是基礎(chǔ)價,佐料又要寧外算錢。(真是聰明)
現(xiàn)在當(dāng)前的系統(tǒng)設(shè)計是:

本來想自己畫UML圖,發(fā)現(xiàn)自己畫也一樣,還損失了書上一些重要信息,所以直接盜圖好了,大家不要介意。
Beverage: 飲料,抽象類,getDescription() 返回描述(類型,佐料...),cost() 抽象方法,每種飲料話費不一樣,所以子類自己去實現(xiàn)。
然后派生了4中飲料的子類: HoseBlend, DarkRoast, Decaf, Espresso(尼瑪,我都沒喝過),分別實現(xiàn) cost方法,返回價格。
代碼很簡單,這里就不貼了,然后這個時候,如果飲料有100種,呵呵,這種設(shè)計類圖就是這樣:

類爆炸!這種設(shè)計,明顯重用率太低,好吧,換種思路,我們把所有的佐料的放到公共父類中,讓所有子類都擁有所有的佐料,只是在類中判斷到底加沒加,例如這樣:

代碼:
Beverage .java
public abstract class Beverage {
protected String description;
protected boolean milk;
protected boolean soy;
protected boolean mocha;
protected boolean whip;
public Beverage(){
this.description = "unknown beverage";
}
public String getDescription() {
return description;
}
public abstract double cost();
}
HoseBlend.java
public class HoseBlend extends Beverage {
public HoseBlend() {
description = "HoseBlend";
milk = true;
soy = false;
mocha = false;
whip = false;
}
public double cost() {
double money = 10.0;
if (milk)
money += 2.0;
if (soy)
money += 3.0;
if (mocha)
money += 3.0;
if (whip)
money += 2.0;
return money;
}
}
使用:
Beverage hoseBlend = new HoseBlend();
System.out.println(hoseBlend.getDescription());
System.out.println(hoseBlend.cost());
其他類省略了,當(dāng)然這里寫得不規(guī)范,每種佐料的價格應(yīng)該寫成常量,這里直接用了數(shù)字,這里不是重點。這樣的類設(shè)計出來,我們可以用一段話來描述:一個抽象的Beverage類,里面包含了很多佐料,子類決定是否添加這些佐料,并且計算初始價格和佐料價格。
與前一種不同的是,這種更加規(guī)范,父類決定了所有的佐料和價格,只是你填不填加,自己決定。突然想到了一種更好的方法,為何不讓Beverage去計算價格呢?
Beverage.java
public abstract class Beverage {
protected String description;
protected double money;
public Beverage(){
this.description = "unknown beverage";
this.money = 10.0;
}
public String getDescription() {
return description;
}
public double cost(){
return money;
}
public void addMilk() {
this.money += 2.0;
}
public void addSoy(){
this.money += 3.0;
}
public void addMocha(){
this.money += 3.0;
}
public void addWhip(){
this.money += 2.0;
}
}
添加4中 addXXX() 方法,然后計算money,將計算價格留給父類,子類只需要添加佐料:
HoseBlend .java
public class HoseBlend extends Beverage {
public HoseBlend() {
description = "HoseBlend";
addMilk();
}
}
感覺這樣寫重用可以更好,并且子類工作也更少了。呵呵,書上沒寫,自己瞎想的。但是上面的這種設(shè)計,當(dāng)遇到添加一種佐料時,都必須修改Beverate類,在設(shè)計模式原則中有一個非常重要的原則,就是開閉原則:對擴(kuò)展開放,對修改關(guān)閉。上面的幾種設(shè)計都存在一定的問題。我們來看看今天的主角,裝飾器模式,怎么來完成。
裝飾器模式
先來看一張形象的圖:

最里層的 DarkRoast 是飲料類型外面添加一層 Mocha, 再外層添加 Whip。最終cost() 就從最外層,一直調(diào)用到最里層,累加得到價格,呵呵,是不是有點像遞歸,對多態(tài)的遞歸,看看類結(jié)構(gòu):

當(dāng)然這是在最初,我們類爆炸那個例子中的結(jié)構(gòu)擴(kuò)展的:
Beverage依然是那個抽象類,依然有4種飲料繼承與它。不同的是,多了一個 CondimentDecorator,繼承于 Beverage,然后4種佐料都繼承與 CondimentDecorator,并都包含一個 beverage 的引用,表示自己裝飾的對象。
好了,看看代碼:
Beverage .java 還是長這樣
public abstract class Beverage {
protected String description;
public Beverage(){
this.description = "unknown beverage";
}
public String getDescription() {
return description;
}
public abstract double cost();
}
HouseBlend .java 第一種飲料,最低10.0元
public class HouseBlend extends Beverage {
public HouseBlend(){
description = "HoseBlend";
}
public double cost() {
return 10.0;
}
}
DarkRoast.java 第二種飲料,最低12.0元
public class DarkRoast extends Beverage {
public DarkRoast(){
description = "DarkRoast";
}
public double cost() {
return 12.0;
}
}
CondimentDecorator .java 讓子類都重寫getDescription() 方法
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
Milk.java : 牛奶,每加一份2.0 元
public class Milk extends CondimentDecorator {
Beverage beverage;
public Milk(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return this.beverage.getDescription() + "," + "Milk";
}
public double cost() {
return this.beverage.cost() + 2.0;
}
}
Mocha.java 摩卡,每份3.0元
public class Mocha extends CondimentDecorator {
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return this.beverage.getDescription() + "," + "Mocha";
}
public double cost() {
return this.beverage.cost() + 3.0;
}
}
Soy.java 醬油,每份3.0元
public class Soy extends CondimentDecorator {
Beverage beverage;
public Soy(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return this.beverage.getDescription() + "," + "Soy";
}
public double cost() {
return this.beverage.cost() + 3.0;
}
}
好了,看看我們怎么調(diào)用:
public class Main {
public static void main(String[] args) {
Beverage b1 = new HouseBlend();
System.out.println(b1.getDescription() + " $" + b1.cost());
Beverage b2 = new DarkRoast();
b2 = new Mocha(b2);
b2 = new Soy(b2);
b2 = new Milk(b2);
System.out.println(b2.getDescription() + " $" + b2.cost());
}
}
輸出:
HoseBlend $10.0
DarkRoast,Mocha,Soy,Milk $20.0
總結(jié)
- 解耦合了吧,飲料和佐料分開,想怎么加怎么加,如果要添加新飲料或者佐料,只需繼續(xù)添加,而無需修改以前的結(jié)構(gòu)。這就是對擴(kuò)展開放,對修改關(guān)閉。
- 我前面提到了一句話:遞歸多態(tài),哈哈哈,自己瞎編的!為什么會出現(xiàn)這種結(jié)果,如果你對多態(tài)熟悉的話,就很好理解了,看上面代碼的 b2:
- 為什么 Milk 可以賦值給 Beverage , 因為 Milk 的父類繼承于 Beverage
- b2調(diào)用 cost() 是調(diào)用 Milk 的 cost() = 2.0+ this.beverage.cost(), this.beverage指向的是 上一個b2,及 Soy, Soy調(diào)用cost(), 及調(diào)用 Mocha.cost() + 3.0 ... , 知道最后調(diào)用 beverage 的 cost() , 是不是很像遞歸。
再一句話概括裝飾者模式吧:
裝飾者模式,就是裝飾者(Docorator)需要繼承與被裝飾者(Component),并且持有被裝飾者的引用(Component),從而可以通過復(fù)寫,在原來 Component 的基礎(chǔ)上對方法做一定的修飾。