按照單一職責原則,某一個對象只專注於干一件事,而如果要擴展其職能的話,不如想辦法分離出一個類來“包裝”這個對象,而這個擴展出的類則專注於實現擴展功能。
裝飾器模式就可以將新功能動態地附加於現有對象而不改變現有對象的功能。
1.裝飾器模式
實際上Java提供的工具包中,IO相關工具就普遍大量使用了裝飾器模式,例如充當裝飾功能的IO類如BufferedInputStream等,又被稱為高級流,通常將基本流作為高級流構造器的參數傳入,將其作為高級流的一個關聯對象,從而對其功能進行擴展和裝飾。
裝飾器模式(Decorator Pattern),動態地給一個對象添加一些額外的職責,就增加功能來說,裝飾器模式比生成子類更靈活。 ----《大話設計模式》
裝飾器模式使用分層對象,動態透明地對單個對象添加職責。 下面是裝飾器模式的UML類圖:
裝飾器實現修飾對象(Component)的接口,所有請求都轉發給它處理,在轉發請求之前/之后增加額外功能。使用步驟是:
- 用一個Decorator實現/繼承需要修飾的對象Component;
- 在Decorator中增加一個Component的引用;
- 在Decorator的構造器中,增加一個Component參數來初始化Component;
- 在Decorator類中,使用Component的引用,將所有請求轉發至Component的相應方法;
- ConcreteDecorator中所有Override自Component的方法做相應調整。
從類圖上看,裝飾器模式與代理模式很像,是它們的目的不同,所以使用方法和適用場景上也就不同 ,裝飾器模式與代理模式的區別:
- 代理模式專注於對被代理對象的訪問;
- 裝飾器模式專注於對被裝飾對象附加額外功能。
就像前面所說的io工具包,我用BufferedInputStream和用FileInputStream去read一個文件實際使用方式上是一樣的,能用FileInputStream.read(),就能用BufferedInputStream.read(),只不過,BufferedInputStream把FileInputStream包裝了一下,增加了一個緩存,並不控制底層FileInputStream的read()行為。
2. 適用場景
- 運行時,你需要動態地為對象增加額外職責時;
- 當你需要一個能夠代替子類的類,借助它提供額外方法時。
3.代碼實現
假設我去買咖啡,首先服務員給我沖了一杯原味咖啡,我希望服務員給我加些牛奶和白糖混合入原味咖啡中。使用裝飾器模式就可以解決這個問題。
咖啡接口,定義了獲取花費和配料的接口。
/** * 咖啡 */ interface Coffee { /** 獲取價格 */ double getCost(); /** 獲取配料 */ String getIngredients(); }
原味咖啡,實現Coffe接口,花費1元,配料中,只有咖啡
/** * 原味咖啡 */ class SimpleCoffee implements Coffee { @Override public double getCost() { return 1; } @Override public String getIngredients() { return "Coffee"; } }
咖啡對象的裝飾器類,同樣實現Coffee接口,定義一個Coffe對象的引用,在構造器中進行初始化。並且將getCost()和getIntegredients()方法轉發給被裝飾對象。
/** * 咖啡的"裝飾器",可以給咖啡添加各種"配料" */ abstract class CoffeeDecorator implements Coffee { protected final Coffee decoratedCoffee; /** * 在構造方法中,初始化咖啡對象的引用 */ public CoffeeDecorator(Coffee coffee) { decoratedCoffee = coffee; } /** * 裝飾器父類中直接轉發"請求"至引用對象 */ public double getCost() { return decoratedCoffee.getCost(); } public String getIngredients() { return decoratedCoffee.getIngredients(); } }
具體的裝飾器類,負責往咖啡中“添加”牛奶,注意看getCost()方法和getIngredients()方法,可以在轉發請求之前或者之后,增加功能。如果是代理模式,這里的結構就有所不同,通常代理模式根據運行時的條件來判斷是否轉發請求。
/** * 此裝飾類混合"牛奶"到原味咖啡中 */ class WithMilk extends CoffeeDecorator { public WithMilk(Coffee coffee) { super(coffee); } @Override public double getCost() { double additionalCost = 0.5; return super.getCost() + additionalCost; } @Override public String getIngredients() { String additionalIngredient = "milk"; return super.getIngredients() + ", " + additionalIngredient; } }
另一個具體裝飾器類,用來給咖啡加糖,一樣的邏輯。
class WithSugar extends CoffeeDecorator { public WithSugar(Coffee coffee) { super(coffee); } @Override public double getCost() { return super.getCost() + 1; } @Override public String getIngredients() { return super.getIngredients() + ", Sugar"; } }
客戶端使用裝飾器模式,是不是與java中的io使用方式很像?
public class DecoratorDemo { static void print(Coffee c) { System.out.println("花費了: " + c.getCost()); System.out.println("配料: " + c.getIngredients()); System.out.println("============"); } public static void main(String[] args) { //原味咖啡 Coffee c = new SimpleCoffee(); print(c); //增加牛奶的咖啡 c = new WithMilk(c); print(c); //再加一點糖 c = new WithSugar(c); print(c); } }
輸出結果:
花費了: 1.0 配料: Coffee ============ 花費了: 1.5 配料: Coffee, milk ============ 花費了: 2.5 配料: Coffee, milk, Sugar ============
4.總結
從上個例子可以看出,裝飾器模式的結構很像代理模式,裝飾器模式的請求轉發過程很像職責煉模式,只不過:
- 職責鏈模式在轉發請求過程中,最多只有一個對象會處理請求,而裝飾器模式則有多個對象處一個請求。
裝飾器模式是代替增加子類的一種解決方案,體現了聚合/合成復用原則的思想,盡量使用組合的方式來擴展功能,這樣就把基本功能和擴展功能解耦了,使得代碼可復用,可維護,靈活。關鍵點在於裝飾器模式可以動態地為對象增加擴展功能。