裝飾模式——結構型模式(4)


前言

上一篇,我們詳細講解了組合模式,回顧一下:其主要將對象組合成樹形結構以表示“部分——整體”的層次結構,這樣可以使得用戶對單個對象和組合對象的使用具有一致性,因為它們都遵循一套相同的接口,無須區別對待;相對於安全式的實現,透明性的實現方式通常是更好的選擇,因為它真正符合了組合模式的本質意圖。對象組合是組合模式的實現的根本,今天我們將要講解的模式也同樣是通過對象組合的方式來實現,也是將Favor Composition Over Inheritance原則演繹到極致的一種模式,下面就讓我們揭開它的神秘面紗吧。

動機

在實際的軟件開發中,我們通過會考慮通過繼承的方式來擴展對象的功能,但是由於繼承為類型增加了靜態特質,在未來擴展之時不夠靈活、方便,同時隨着子類的增加(擴展的功能子類),各種功能子類的組合勢必會導致更加子類的出現,呈現出“類爆炸”的棘手場景,不管從管理上還是從擴展上來說,這都會是開發人員的惡夢。如何提供一種封裝機制,將“對象功能的擴展”能夠根據需求動態的增加和刪除?又能很好地避免由於“對象功能的擴展”引入的子類激增的問題?將“功能擴展的需求”所導致的影響降至最低限度,這就是我們今天所要重點講述的裝飾模式的用武之地呢!

意圖

動態地給一個對象添加一些額外的職責。就增加功能來說,裝飾模式相比生成子類更為靈活。

既然是給對象添加額外的職責,自然就不應該改變對象的類型,更不應該改變對象的接口呢。換句話來說就是我們應該是透明地給一個對象增加功能,不能讓這個對象知道,也就是不能去改動這個對象。我們只需要為對象添加的職責編寫一個類,用於完成這個職責的具體實現,至於對象的核心職責還是委托轉調給這個被裝飾的對象,這樣我們就有效地將類的核心職責與裝飾功能(額外功能)區分開呢,也有利於去除重復的裝飾邏輯。

結構圖

image

  1. 抽象組件(Component)角色:組件對象的接口,可以給這些對象動態地添加職責。
  2. 具體組件(ConcreteComponent)角色:具體的組件對象,實現組件對象接口,通過就是被裝飾器裝飾的原始對象,也就是可以給這個對象添加額外職責。
  3. 裝飾器(Decorator)角色:所有裝飾器的抽象父類,需要定義一個與組件接口相一致的接口,並持有一個Component對象,本質上就是持有一個被裝飾的對象。
  4. 具體裝飾器(ConcreteDecorator)角色:實際的裝飾器對象,實現具體的要向被告裝飾對象添加的額外功能。

代碼示例

 1:  public abstract class Component{
 2:      public abstract void Operation();
 3:  }
 4:   
 5:  public class ConcreteComponent extends Component{
 6:      public void Operation(){
 7:          System.out.println("ConcreteComponent is doing!");
 8:      }
 9:  }
10:   
11:  public abstract class Decorator extends Component{
12:      protected Component component;
13:   
14:      public Decorator(Component component){
15:          this.component=component;
16:      }
17:   
18:      public void setComponent(Component component) {
19:          this.component = component;
20:      }
21:   
22:      public void Operation(){
23:          component.Operation();
24:      }
25:  }
26:   
27:  public class ConcreteDecoratorA extends Decorator{
28:      public ConcreteDecoratorA(Component component){
29:          super(component);
30:      }
31:      private String addState;
32:      public String getAddState() {
33:          return addState;
34:      }
35:      public void setAddState(String addState) {
36:          this.addState = addState;
37:      }
38:   
39:      public void Operation(){
40:          //調用相關狀態信息
41:          getAddState();
42:          component.Operation();
43:      }
44:  }
45:   
46:  public class ConcreteDecoratorB extends Decorator{
47:      public ConcreteDecoratorB(Component component){
48:          super(component);
49:      }
50:   
51:      public void Addhavior(){
52:          System.out.println("add a Behavior!");
53:      }
54:   
55:      public void Operation(){
56:          component.Operation();
57:          Addhavior();
58:      }
59:  }
60:   
61:  public class Client{
62:      public static void main(String[] args){
63:          Component component=new ConcreteComponent();
64:   
65:          Decorator decorator1=new ConcreteDecoratorA(component);
66:          decorator1.Operation();
67:   
68:          //直接給裝飾對象進行裝飾,這樣decorator2就同時具有兩個額外功能呢
69:          Decorator decorator2=new ConcreteDecoratorB(decorator1);
70:          decorator2.Operation();    
71:      }
72:  }

從示例代碼中,我們可以清楚地理解裝飾模式的基本實現方式。首先,抽象組件Component類定義了組件對象的接口,而具體組件ConcreteComponent實現了組件接口,是真正的組件。緊接着是關鍵的裝飾器接口的定義,它繼承了抽象組件接口,同時還持有一個組件接口對象的引用。之所以要繼承組件接口,目的是為了讓裝飾對象與組件對象保持相同的接口,也就是類型一致,這樣方便客戶端無差別地操作組件對象和裝飾對象,而持有一組件接口對象的主要原因是為了調用組件的核心職責,畢竟,裝飾品的目的主要是給具體的組件對象添加額外職責,核心的功能還是交給組件對象本身來完成。綜合二者,可以看出,裝飾器接口的定義關鍵是為透明地給組件對象添加額外功能。在這里,需要強調的是,各個裝飾器之間最好是完全獨立的功能,彼此間不應該存在依賴,唯有這樣,在進行裝飾組合的時候,才沒有先后順序的限制,也就是說無論先裝飾誰或者后裝飾誰結果應該都是一致的,否則將會大大地降低裝飾器組合的可擴展性和靈活性。另外,每一個裝飾器的功能粒度不應該太大,這樣更有利於功能的復用性,在將來的可以通過多個裝飾器來組合完成較復雜的額外功能。最后,在示例代碼69行處,裝飾器decorator2同時具有了兩種額外的功能,因為它組合了兩種不同的裝飾器。這也是裝飾模式的精妙之處。

現實場景

在現實生活場景中,也有不少可以抽象為裝飾模式的例子。比如,我們房間里掛的圖畫,它的主要功能是為了美化我們房間,讓進入房間的我們更加賞心悅目,心情愉快。通常為了更好地保存這樣一幅圖畫,我們會為其制作一邊框,防止它輕易地被損壞;另外,如果圖畫由於某種客觀原因不適合掛在牆壁上,我們也會考慮為其制作一支架,將其固定在房間的某一位置上,防止圖畫被摔壞等意外事件。從上面的例子中,我們可以看到,不管是為圖畫制作邊框還是制作支架,都只是為圖畫自身添加額外的功能罷了,其本身的核心功能我們並沒有改變,之所以添加這樣那樣的職能,主要是為了更方便我們對圖畫的使用和欣賞而已。這里需要提醒的是,為圖畫添加邊框或者支架兩者是沒有關聯或者依賴的,彼此是獨立的,也就是說完全可以分開添加亦可以一起添加,這只取決於我們自己的意願而已。這里,添加邊框和支架相對於圖畫來說就是兩個裝飾器,用於裝飾圖畫。說了這么多,還是讓我們用代碼來演繹下其基本的實現過程吧。

 1:  public abstract class Picture{
 2:      public abstract void Show();
 3:  }
 4:   
 5:  public  class Canvas extends Picture{
 6:      public void Show(){
 7:          System.out.println("Canvas is showing!");
 8:      }
 9:  }
10:   
11:  public abstract class Decorator extends Picture{
12:      protected Picture picture;
13:      public Decorator(Picture picture){
14:          this.picture=picture;
15:      }
16:   
17:      public void Show(){
18:          picture.Show();
19:      }
20:  }
21:   
22:  public class Frame extends Decorator{
23:      public Frame(Picture picture){
24:          super(picture);
25:      }
26:   
27:      private void addFrame(){
28:          System.out.println("frame is added to the picture!");
29:      }
30:   
31:      public void Show(){
32:          addFrame();
33:          super.Show();
34:      }
35:   
36:  }
37:   
38:  public class Carrier extends Decorator{
39:      public Carrier(Picture picture){
40:          super(picture);
41:      }
42:   
43:      private void makeCarrier(){
44:          System.out.println("Carrier is made for picture!");
45:      }
46:   
47:      public void Show(){
48:          makeCarrier();
49:          super.Show();
50:      }
51:  }
52:   
53:   
54:  public class Client{
55:      public static void main(String[] args){
56:          Picture picture=new Canvas();
57:          Decorator frame=new Frame(picture);
58:          frame.Show();
59:   
60:          Decorator carrier=new Carrier(frame);
61:          carrier.Show();        
62:      }
63:  }    

可以看到,上面的代碼與上文的示例代碼結構上幾乎保持一致,這里,我們也就不再重復敘述呢。提醒一點是,代碼60行,裝飾器對象carrier此時已經同時擁有了兩項額外職責,添加邊框和添加支架的功能,因為它對直接對Frame裝飾器進行了裝飾,自然也就擁有了邊框裝飾器所支持的功能呢。下圖是示例的類結構圖:

 image

在java語言的世界里,I/O流應該是裝飾模式最典型的應用之一呢。回憶一下,我們通過會如何使用流式操作讀取文件內容的呢?下面是簡單的代碼示例:

 1:  public static void main(String[] args) throws IOException{
 2:      DataInputStream din=null;
 3:      try {
 4:          din=new DataInputStream(
 5:                  new BufferedInputStream(
 6:                          new FileInputStream("Test.txt")));
 7:   
 8:          byte bs[]=new byte[din.available()];
 9:          din.read(bs);
10:          String content=new String(bs);
11:          System.out.println("文件內容為: "+content);
12:      } finally{
13:          din.close();
14:      }
15:  }

從上述代碼中,我們可以看到,最底層的FileInputStream外層被兩個裝飾器裝飾着,一個是DataInputStream,一個是BufferedInputStream。FileInputStream對象相當於最原始的被裝飾組件對象,而BufferedInputStream對象和FileInputStream對象則相當裝飾器,示例代碼其實就是裝飾器的組裝過程。大家可能對java中的I/O結構體系並不清楚,可以明確的是,既然I/O流可以通過裝飾器模式來組裝,那就說明裝飾器與具體的組件類要實現相同的接口,下面的類結構圖便是java中I/O類圖關系,通過它,大家應該就很清楚呢,與裝飾模式結構圖基本一致,這里我們省去了各個類中接口方法。

 image

從上圖,我們可以發現,I/O類結構與裝飾模式結構幾乎是一樣的:

  1. InputStream就相當於裝飾模式中的Component
  2. FileInputStream、ObjectInputStream、StringBufferInputStream都實現了InputStream接口,所以它們相當於裝飾模式的具體組件類(ConcreteComponent)。
  3. FilterInputStream不僅實現了InputStream接口,還持有InputStream接口對象引用,其實就是裝飾模式的Decorator角色,而繼承於FilterInputStream的DataInputStream、BufferedInputStream、LineNumberInputStream、PushbackInputStream就是具體的裝飾器對象。

好呢,對裝飾模式的現實場景舉例就說到這呢,相信通過上面兩個例子,大家應該也比較清楚裝飾模式的基本使用呢。

實現要點

  1. 保持接口的一致性。裝飾對象的接口必須與它所裝飾品的組件接口保持一致,故所有的具體裝飾器都是應該實現同一個公共父類,即Decorator類。而Decorator又繼承於抽象組件類Component,因此所有的裝飾對象也完全屬於組件類型范疇。這樣做的好處是Decorator對Component是透明的,Component無須知道Decorator的存在,Decorator是從外部來擴展Component功能。
  2. 可省略抽象Decorator類。當我們僅需要給組件添加一個職責或功能時,完全沒必要定義抽象Decorator類。這時,可以直接把Decorator向Component轉發請求的職責合並到具體裝飾器對象(ConcreteDecorator)中。
  3. 保持Component類的簡單性。為了保證接口的一致性,組件和裝飾必須有一個公共的Component類。因而,保持這個類的簡單性是很重要的,它應該集中定義接口而不是存儲數據,對數據表示的定義延遲到子類,否則Component會變得復雜和龐大,也難以使用。關鍵是賜予Component太多的功能,對於子類來說未必需要,只會造成臃腫的糟糕設計而已。
  4. 改變對象外殼和改變對象內核。Component可以說是組件的內核,而其具體實現由ConcreteComponent來完成,Decorator是組件的外殼,用於改變組件的外在表示和行為。裝飾模式主要用於完成對組件外殼的改變,而內核的改變通過是一個改變組件內核的很好模式。不過當Component類原本就比較龐大、復雜時,使用裝飾模式代價較高,此時策略模式相對會合適一些,我們可以將組件的一些行為轉發給一個獨立的策略對象,只需要替換相應的策略對象,就可以改變或者擴充組件的功能。

運用效果

  1. 比靜態繼承更靈活。與對象的靜態繼承相比,裝飾模式提供了更加靈活的向對象添加職責的方式,可以在運行時刻增加和刪除職責。相比之下,繼承機制要求為每個添加的職責都創建一個新的子類,這樣會產生很多新類,增加系統的復雜度。此外,我們可以提供不同的Decorator,這就使得我們可以對一些職責進行混合和匹配。
  2. 避免在層次結構高層的類有太多的特征。裝飾模式提供了一種“即用即付”的方式來添加職責。這並不需要在一個復雜的可定制的類中支持所有可預見的特征,相反,我們完全可以具體的裝飾類給裝飾對象逐漸地添加各種功能,如此應用程序不必為不需要的功能而付出不必要的代價。
  3. Decorator與Component不一樣。Decorator是一個透明的包裝。如果我們從對象標識的觀點出發,一個被裝飾的了的組件與這個組件是有差別的,因此,使用裝飾時不應該依賴對象標識。
  4. 會產生很多小對象。采用裝飾模式進行系統設計往往會產生許多看一去類似的小對象,也就是各種具體的裝飾類對象。新手學習這樣的系統會相較困難,排錯也不方便。

適用性

  1. 在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職責。因為裝飾器實現了組件接口,與具體的組件對象屬於同一類型,故用戶可以無區別地操作組件對象和裝飾器對象。
  2. 處理那些可以撤銷的職責。因為添加的職責只是額外職責,並非組件對象的核心職責,完全可以在不需要的時候進行撤銷和替換它們。
  3. 當不能采用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴展,為支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因為類定義被隱藏,或類定義不能用於生成子類。

相關模式

  1. 裝飾模式與組合模式:兩者有相似之處,都涉及對象的遞歸調用,從某個角度來說,可以把裝飾看作只有一個組件的組合。但是兩者的目的是完全不一樣的,裝飾模式是要動態地透明地給對象增加功能,而組合模式是要管理基本對象和組合對象,為它們提供一個一致的接口給客戶端,方便用戶使用。
  2. 裝飾模式與策略模式:策略模式亦可以實現動態地改變對象的功能,但是策略模式只是一層選擇,也就是根據策略選擇一下具體的實現類而已。而裝飾模式不只一層,而是遞歸調用,無數層都可以,只要組合好裝飾器的對象組合,就可以集資調用下去,所以裝飾械更靈活些。當然,我們完全可以將兩者組合在一起使用,在一個具體的裝飾器中使用策略模式來選擇更具體的實現方式。
  3. 裝飾模式與模板方式:模式方法主要應用在算法骨架固定的情況,如果算法不固定就可以使用裝飾模式,因為在使用裝飾模式的時候,進行裝飾器的組裝時,其實就是一個調用算法步驟的組裝,相當於一個動態的算法骨架。當然這只是模仿功能而已,兩個設計模式的目的、功能和本質思想都是不一樣的。這點大家需要明確。

總結

裝飾模式的本質是:動態組合。動態是手段,組合才是目的。這里的組合有兩層意義,一個是動態功能的組合,也就是動態進行裝飾器的組合;另一個是指對象組合,通過對象組合來為被裝飾對象透明地增加功能。此外裝飾模式不僅可以增加功能,亦可以控制功能的訪問,完全實現新的功能,同時也可以控制裝飾的功能是在裝飾功能之前還是之后立即來運行等。總之,裝飾模式是通過把復雜功能簡單化,分散化,然后在運行期間,根據需要動態組合相應的裝飾器,獲取相應的職責,這也是為什么需要將裝飾器功能盡量細粒度化的原因,有利於復用。到這里,對裝飾模式的講解也已經接近尾聲呢,下一篇,我們將繼續學習另一種結構型模式——外觀模式,敬請期待!

參考資料

  1. 程傑著《大話設計模式》一書
  2. 陳臣等著《研磨設計模式》一書
  3. GOF著《設計模式》一書
  4. Terrylee .Net設計模式系列文章
  5. 呂震宇老師 設計模式系列文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM