設計原則:
- 少用繼承,多用組合
- 類應該對擴展開放,對修改關閉
目錄
本文的結構如下:
- 什麼是裝飾者模式
- 爲什麼要用該模式
- 模式的結構
- 代碼示例
- 優點和缺點
- 適用環境
- 總結
一、什麼是裝飾模式
裝飾者模式動態地將責任附加到對象上。若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案。和代理模式很相似,但在對被裝飾的對象的控制程度是不同的;裝飾者模式是對對象功能的加強,而代理模式是對對象施加控制,並不提供對對象本身功能的加強。
通俗點講,假設現在一杯純豆漿(Soya)賣1元錢,你可以選擇往裏邊加糖0.5元(Suger),加蜂蜜0.5元(Honey),加牛奶1元(Milk),加黑豆1元(BlackBean)。這裏面純豆漿就是被裝飾者,而糖、蜂蜜、牛奶、黑豆就是裝飾者了。
二、爲什麼要用該模式
通過繼承的方式可以使子類具有父類的屬性和方法。子類繼承父類後,因爲一些業務需求可以通過重寫的方式來加強父類的方法的一些功能,也可以重新定義某些屬性,即覆蓋父類的原有屬性和方法,使其獲得與父類不同的功能。
而裝飾者模式的最基本的功能就是對傳入的一個對象進行功能的加強與優化。
那麼問題來了,既然繼承方式也可以對已有類或對象進行加強,那爲什麼還要衍生出裝飾者模式這一思想呢?
裝飾者模式的意圖定義爲:動態地給一個對象添加一些額外的職責。裝飾者模式存在的更重要的意義就在於動態的爲對象添加一些功能(或分配額外職責)。
以豆漿的例子爲例,先嚐試用繼承的方式分析一下:設豆漿類爲基類,每個類中有一個money屬性,那麼豆漿加牛奶可模擬爲Soya類繼承Milk並重寫pay()方法,如此繼承確實可以計算出每種組合的價錢,於是有下圖:
一般的,我們爲了擴展一個類經常使用繼承方式實現,由於繼承爲類引入靜態特徵,並且隨着擴展功能的增多,子類會很膨脹。
在不想增加很多子類的情況下擴展類,如何實現呢?這時就要用到今天的主角:裝飾者模式了。
Java中的IO機制就用到了裝飾者模式。比如最常用的語句:
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filepath)));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
這就是最常見的裝飾者模式了,通過BufferedReader對已有對象FileReader的功能進行加強和優化。其實它不僅可以加強FileReader,所有的字符輸入流都可以通過這種方式進行包裝。
它是如何實現的呢?
將所有的字符輸入流抽象出了一個基類或接口即Reader,然後通過構造方法的形式將Reader傳遞給BufferedReader,此時BufferedReader就可以對所有的字符輸入流進行攔截和優化了。
如果採用繼承機制,每個XXXReader就要衍生出一個BufferedXXXReader,再加上字符輸出流和字節輸入輸出流,那麼Java的IO體系結構該是多麼的臃腫不堪啊!而裝飾者模式的出現解決了這個問題,並且,裝飾者的出現也再一次的證明了面向對象的設計原則:多用組合,少用繼承!對擴展開放,對修改關閉!
三、模式的結構
- Component,給出一個抽象接口或者抽象類,規範實現類的一些方法;
- ConcreteComponent:具體的一個組件,實現了抽象方法;
- Decorator:抽象的裝飾者,對抽象接口或者抽象類的一個引用,在 method 方法裏面使用這個引用完成任務;(代理模式需要實例化)
- ConcreteDecorator:具體的裝飾者,對抽象裝飾者的抽象部分進行實現。
第一步:抽象組件
/**
* 抽象組件
*
* Created by w1992wishes on 2017/10/30.
*/
public abstract class Component {
public abstract void method();
}
第二步:具體組件
/**
* 具體組件
*
* Created by w1992wishes on 2017/10/30.
*/
public class ConcreteComponent extends Component {
public void method() {
System.out.println("work");
}
}
第三步:抽象裝飾者
/**
* 抽象的裝飾者
*
* Created by w1992wishes on 2017/10/30.
*/
public abstract class Decorator extends Component{
private Component component;
public Decorator(Component component){
this.component = component;
}
@Override
public void method(){
beforeM();
component.method();
afterM();
}
public abstract void beforeM();
public abstract void afterM();
}
第四步:具體的抽象者
/**
* 具體的裝飾者
*
* Created by w1992wishes on 2017/10/30.
*/
public class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component){
super(component);
}
@Override
public void beforeM() {
System.out.println("Relax, first having a game before work!");
}
public void afterM() {
System.out.println("Relax, first having a game after work!");
}
}
第五步:客戶端運行
/**
* Created by w1992wishes on 2017/10/30.
*/
public class Client {
public static void main(String[] args) {
Component c = new ConcreteComponent();
Decorator d = new ConcreteDecorator(c);
d.method();
}
}
結果:
Relax, first having a game before work!
work
Relax, first having a game after work!
四、代碼示例
前面其實可以看作是一個代碼示例,下面接着用豆漿的例子來段代碼(這裏使用抽象接口):
首先抽象出一個接口,作爲裝飾者構造函數的參數,即被裝飾者的父類:
/**
* Created by w1992wishes on 2017/10/30.
*/
public interface Drink {
public float money();//獲取價格。
public String description();//返回商品信息。
}
接下來就是裝飾者類,繼承此接口並通過構造方法獲取被裝飾對象公共接口:
/**
* Created by w1992wishes on 2017/10/30.
*/
public abstract class Decorator implements Drink{
private Drink drink;
public Decorator (Drink drink){
this.drink = drink;
}
@Override
public String description() {
return drink.description();
}
@Override
public float money() {
return drink.money();
}
}
下面是被裝飾者Soya:
/**
* 具體的裝飾者對象,純豆漿
*
* Created by w1992wishes on 2017/10/30.
*/
public class Soya implements Drink {
public String description() {
return "純豆漿";
}
public float money() {
return 2f;
}
}
下面分別是裝飾者Suger,Milk,BlackBean,Honey類:
Suger
/**
* 具體的裝飾者類:糖
*
* Created by w1992wishes on 2017/10/30.
*/
public class Suger extends Decorator {
public Suger(Drink drink) {
super(drink);
}
public String description() {
return super.description()+"+糖";
}
public float money() {
return super.money()+1.5f;
}
}
Milk
/**
* 具體的裝飾者對象:牛奶
*
* Created by w1992wishes on 2017/10/30.
*/
public class Milk extends Decorator {
public Milk(Drink drink) {
super(drink);
}
public String description() {
return super.description()+"+牛奶";
}
public float money() {
return super.money()+1.5f;
}
}
Honey
/**
* 具體的裝飾者類:蜂蜜
*
* Created by w1992wishes on 2017/10/30.
*/
public class Honey extends Decorator {
public Honey(Drink drink) {
super(drink);
}
public String description() {
return super.description()+"+蜂蜜";
}
public float money() {
return super.money()+1.5f;
}
}
BlackBean
/**
* 具體的裝飾者對象:黑豆
*
* Created by w1992wishes on 2017/10/30.
*/
public class BlackBean extends Decorator {
public BlackBean(Drink drink) {
super(drink);
}
public String description() {
return super.description()+"+黑豆";
}
public float money() {
return super.money()+0.5f;
}
}
最後是測試代碼:
/**
* Created by w1992wishes on 2017/10/30.
*/
public class Client {
public static void main(String[] args) {
//定義一杯純豆漿
Soya soya = new Soya();
System.out.print("-----------------");
System.out.println(soya.description()+" 價錢:"+soya.money());
//豆漿加奶
Milk milkSoya = new Milk(soya);
System.out.print("-----------------");
System.out.println(milkSoya.description()+" 價錢:"+milkSoya.money());
//黑豆豆漿+奶
BlackBean beanMilkSoya = new BlackBean(milkSoya);
System.out.print("-----------------");
System.out.println(beanMilkSoya.description()+" 價錢:"+beanMilkSoya.money());
//黑豆豆漿+奶+蜂蜜
Honey honeyBeanMilkSoya = new Honey(beanMilkSoya);
System.out.print("-----------------");
System.out.println(honeyBeanMilkSoya.description()+" 價錢:"+honeyBeanMilkSoya.money());
}
}
結果:
—————–純豆漿 價錢:2.0
—————–純豆漿+牛奶 價錢:3.5
—————–純豆漿+牛奶+黑豆 價錢:4.0
—————–純豆漿+牛奶+黑豆+蜂蜜 價錢:5.5
五、優點和缺點
5.1、優點:
- 裝飾模式與繼承關係的目的都是要擴展對象的功能,但是裝飾模式可以提供比繼承更多的靈活性。
- 可以通過一種動態的方式來擴展一個對象的功能,通過配置文件可以在運行時選擇不同的裝飾器,從而實現不同的行爲。
- 通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行爲的組合。可以使用多個具體裝飾類來裝飾同一對象,得到功能更爲強大的對象。
- 具體構件類與具體裝飾類可以獨立變化,用戶可以根據需要增加新的具體構件類和具體裝飾類,在使用時再對其進行組合,原有代碼無須改變,符合“開閉原則”。
5.2、缺點:
- 使用裝飾模式進行系統設計時將產生很多小對象,這些對象的區別在於它們之間相互連接的方式有所不同,而不是它們的類或者屬性值有所不同,同時還將產生很多具體裝飾類。這些裝飾類和小對象的產生將增加系統的複雜度,加大學習與理解的難度。
- 這種比繼承更加靈活機動的特性,也同時意味着裝飾模式比繼承更加易於出錯,排錯也很困難,對於多次裝飾的對象,調試時尋找錯誤可能需要逐級排查,較爲煩瑣。
六、適用環境
因爲裝飾者模式的以下特點:
- 裝飾對象和真實對象有相同的接口。這樣客戶端對象就能以和真實對象相同的方式和裝飾對象交互。
- 裝飾對象包含一個真實對象的引用(reference)。
- 裝飾對象接受所有來自客戶端的請求。它把這些請求轉發給真實的對象。
- 裝飾對象可以在轉發這些請求以前或以後增加一些附加功能。這樣就確保了在運行時,不用修改給定對象的結構就可以在外部增加附加的功能。
所以在以下情況下可以使用裝飾模式:
- 在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職責。
- 需要動態地給一個對象增加功能,這些功能也可以動態地被撤銷。
- 當不能採用繼承的方式對系統進行擴充或者採用繼承不利於系統擴展和維護時。不能採用繼承的情況主要有兩類:第一類是系統中存在大量獨立的擴展,爲支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長;第二類是因爲類定義不能繼承(如final類)。
七、總結
- 裝飾模式用於動態地給一個對象增加一些額外的職責,就增加對象功能來說,裝飾模式比生成子類實現更爲靈活。它是一種對象結構型模式。
- 裝飾模式包含四個角色:抽象構件定義了對象的接口,可以給這些對象動態增加職責(方法);具體構件定義了具體的構件對象,實現了在抽象構件中聲明的方法,裝飾器可以給它增加額外的職責(方法);抽象裝飾類是抽象構件類的子類,用於給具體構件增加職責,但是具體職責在其子類中實現;具體裝飾類是抽象裝飾類的子類,負責向構件添加新的職責。
- 使用裝飾模式來實現擴展比繼承更加靈活,它以對客戶透明的方式動態地給一個對象附加更多的責任。裝飾模式可以在不需要創造更多子類的情況下,將對象的功能加以擴展。
- 裝飾模式的主要優點在於可以提供比繼承更多的靈活性,可以通過一種動態的 方式來擴展一個對象的功能,並通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行爲的組合,而且具體構件類與具體裝飾類可以獨立變化,用戶可以根據需要增加新的具體構件類和具體裝飾類;其主要缺點在於使用裝飾模式進行系統設計時將產生很多小對象,而且裝飾模式比繼承更加易於出錯,排錯也很困難,對於多次裝飾的對象,調試時尋找錯誤可能需要逐級排查,較爲煩瑣。
- 裝飾模式適用情況包括:在不影響其他對象的情況下,以動態、透明的方式給 單個對象添加職責;需要動態地給一個對象增加功能,這些功能也可以動態地被撤銷;當不能採用繼承的方式對系統進行擴充或者採用繼承不利於系統擴展和維護時。
- 裝飾模式可分爲透明裝飾模式和半透明裝飾模式:在透明裝飾模式中,要求客 戶端完全針對抽象編程,裝飾模式的透明性要求客戶端程序不應該聲明具體構件類型和具體裝飾類型,而應該全部聲明爲抽象構件類型;半透明裝飾模式允許用戶在客戶端聲明具體裝飾者類型的對象,調用在具體裝飾者中新增的方法。