設計模式 - 裝飾模式
“單一職責”模式
在軟體元件的設計中,如果責任劃分的不清晰,使用繼承得到的結果往往是隨著需求的變化,子類急劇膨脹,同時充斥著重複程式碼,這時候的關鍵是劃清責任。
典型模式
• Decorator
• Bridge
程式碼示例:
1 //業務操作 2 class Stream{ 3 public: 4 virtual char Read(int number)=0; 5 virtual void Seek(int position)=0; 6 virtual void Write(char data)=0; 7 8 virtualdecorator1.cpp~Stream(){} 9 }; 10 11 //主體類 12 class FileStream: public Stream{ 13 public: 14 virtual char Read(int number){ 15 //讀檔案流 16 } 17 virtual void Seek(int position){ 18 //定位檔案流 19 } 20 virtual void Write(char data){ 21 //寫檔案流 22 } 23 24 }; 25 26class NetworkStream :public Stream{ 27 public: 28 virtual char Read(int number){ 29 //讀網路流 30 } 31 virtual void Seek(int position){ 32 //定位網路流 33 } 34 virtual void Write(char data){ 35 //寫網路流 36 } 37 38 }; 39 40 class MemoryStream :public Stream{41 public: 42 virtual char Read(int number){ 43 //讀記憶體流 44 } 45 virtual void Seek(int position){ 46 //定位記憶體流 47 } 48 virtual void Write(char data){ 49 //寫記憶體流 50 } 51 52 }; 53 54 //擴充套件操作 55 class CryptoFileStream :public FileStream{ 56 public: 57 virtual char Read(int number){ 58 59 //額外的加密操作... 60 FileStream::Read(number);//讀檔案流 61 62 } 63 virtual void Seek(int position){ 64 //額外的加密操作... 65 FileStream::Seek(position);//定位檔案流 66 //額外的加密操作... 67 } 68 virtual void Write(byte data){ 69 //額外的加密操作... 70 FileStream::Write(data);//寫檔案流 71 //額外的加密操作... 72 } 73 }; 74 75 class CryptoNetworkStream : :public NetworkStream{ 76 public: 77 virtual char Read(int number){ 78 79 //額外的加密操作... 80 NetworkStream::Read(number);//讀網路流 81 } 82 virtual void Seek(int position){ 83 //額外的加密操作... 84 NetworkStream::Seek(position);//定位網路流 85 //額外的加密操作... 86 } 87 virtual void Write(byte data){ 88 //額外的加密操作... 89 NetworkStream::Write(data);//寫網路流 90 //額外的加密操作... 91 } 92 }; 93 94 class CryptoMemoryStream : public MemoryStream{ 95 public: 96 virtual char Read(int number){ 97 98 //額外的加密操作... 99 MemoryStream::Read(number);//讀記憶體流 100 } 101 virtual void Seek(int position){ 102 //額外的加密操作... 103 MemoryStream::Seek(position);//定位記憶體流 104 //額外的加密操作... 105 } 106 virtual void Write(byte data){ 107 //額外的加密操作... 108 MemoryStream::Write(data);//寫記憶體流 109 //額外的加密操作... 110 } 111 }; 112 113 class BufferedFileStream : public FileStream{ 114 //... 115 }; 116 117 class BufferedNetworkStream : public NetworkStream{ 118 //... 119 }; 120 121 class BufferedMemoryStream : public MemoryStream{ 122 //... 123 } 124 125 126 127 128 class CryptoBufferedFileStream :public FileStream{ 129 public: 130 virtual char Read(int number){ 131 132 //額外的加密操作... 133 //額外的緩衝操作... 134 FileStream::Read(number);//讀檔案流 135 } 136 virtual void Seek(int position){ 137 //額外的加密操作... 138 //額外的緩衝操作... 139 FileStream::Seek(position);//定位檔案流 140 //額外的加密操作... 141 //額外的緩衝操作... 142 } 143 virtual void Write(byte data){ 144 //額外的加密操作... 145 //額外的緩衝操作... 146 FileStream::Write(data);//寫檔案流 147 //額外的加密操作... 148 //額外的緩衝操作... 149 } 150 }; 151 152 153 154 void Process(){ 155 156 //編譯時裝配 157 CryptoFileStream *fs1 = new CryptoFileStream(); 158 159 BufferedFileStream *fs2 = new BufferedFileStream(); 160 161 CryptoBufferedFileStream *fs3 =new CryptoBufferedFileStream(); 162 163 }
流stream作為一個基類有一些基礎的方法,如read,write,seek
各種流檔案流,網路流,記憶體流等繼承於基類流,同時會有擴充套件的需求,如加密,且加密相同,只是讀取的操作不一樣,因此又會有新的不同流的擴充套件類繼承於各種流。當然除了加密還會有其他的擴充套件操作如緩衝buffer,同理也可以像加密一樣繼承。
如果要對一種流既加密又緩衝,可以繼承了流之後在讀寫方法中新增加密操作,緩衝,讀流的基礎操作。
以上的程式碼會有什麼缺陷?
隨著擴充套件方法的增加,由於對繼承的不良使用,子類的規模不斷膨脹,產生很多重複的程式碼。
改進一:使用組合
1 //業務操作 2 class Stream{ 3 4 public: 5 virtual char Read(int number)=0; 6 virtual void Seek(int position)=0; 7 virtual void Write(char data)=0; 8 9 virtual ~Stream(){} 10 }; 11 12 //主體類 13 class FileStream: public Stream{ 14 public: 15 virtual char Read(int number){ 16 //讀檔案流 17 } 18 virtual void Seek(int position){ 19 //定位檔案流 20 } 21 virtual void Write(char data){ 22 //寫檔案流 23 } 24 25 }; 26 27 class NetworkStream :public Stream{ 28 public: 29 virtual char Read(int number){ 30 //讀網路流 31 } 32 virtual void Seek(int position){ 33 //定位網路流 34 } 35 virtual void Write(char data){ 36 //寫網路流 37 } 38 39 }; 40 41 class MemoryStream :public Stream{ 42 public: 43 virtual char Read(int number){ 44 //讀記憶體流 45 } 46 virtual void Seek(int position){ 47 //定位記憶體流 48 } 49 virtual void Write(char data){ 50 //寫記憶體流 51 } 52 53 }; 54 55 //擴充套件操作 56 class CryptoStream: public Stream { 57 58 Stream* stream;//... 59 60 public: 61 CryptoStream(Stream* stm):stream(stm){ 62 63 } 64 65 66 virtual char Read(int number){ 67 68 //額外的加密操作... 69 stream->Read(number);//讀檔案流 70 } 71 virtual void Seek(int position){ 72 //額外的加密操作... 73 stream::Seek(position);//定位檔案流 74 //額外的加密操作... 75 } 76 virtual void Write(byte data){ 77 //額外的加密操作... 78 stream::Write(data);//寫檔案流 79 //額外的加密操作... 80 } 81 }; 82 83 84 85 class BufferedStream : public Stream{ 86 87 Stream* stream;//... 88 89 public: 90 BufferedStream(Stream* stm):stream(stm){ 91 92 } 93 //... 94 }; 95 96 97 void Process(){ 98 99 //執行時裝配 100 FileStream* s1=new FileStream(); 101 CryptoStream* s2=new CryptoStream(s1); 102 103 BufferedStream* s3=new BufferedStream(s1); 104 105 BufferedStream* s4=new BufferedStream(s2); 106 107 108 109 }decorator2.cpp
若多個子類含有相同的欄位,那應該將該欄位向上層提。
改進二:於是在建立一箇中間類Decorator
1 //業務操作 2 class Stream{ 3 4 public: 5 virtual char Read(int number)=0; 6 virtual void Seek(int position)=0; 7 virtual void Write(char data)=0; 8 9 virtual ~Stream(){} 10 }; 11 12 //主體類 13 class FileStream: public Stream{ 14 public: 15 virtual char Read(int number){ 16 //讀檔案流 17 } 18 virtual void Seek(int position){ 19 //定位檔案流 20 } 21 virtual void Write(char data){ 22 //寫檔案流 23 } 24 25 }; 26 27 class NetworkStream :public Stream{ 28 public: 29 virtual char Read(int number){ 30 //讀網路流 31 } 32 virtual void Seek(int position){ 33 //定位網路流 34 } 35 virtual void Write(char data){ 36 //寫網路流 37 } 38 39 }; 40 41 class MemoryStream :public Stream{ 42 public: 43 virtual char Read(int number){ 44 //讀記憶體流 45 } 46 virtual void Seek(int position){ 47 //定位記憶體流 48 } 49 virtual void Write(char data){ 50 //寫記憶體流 51 } 52 53 }; 54 55 //擴充套件操作 56 57 DecoratorStream: public Stream{ 58 protected: 59 Stream* stream;//... 60 61 DecoratorStream(Stream * stm):stream(stm){ 62 63 } 64 65 }; 66 67 class CryptoStream: public DecoratorStream { 68 69 70 public: 71 CryptoStream(Stream* stm):DecoratorStream(stm){ 72 73 } 74 75 76 virtual char Read(int number){ 77 78 //額外的加密操作... 79 stream->Read(number);//讀檔案流 80 } 81 virtual void Seek(int position){ 82 //額外的加密操作... 83 stream::Seek(position);//定位檔案流 84 //額外的加密操作... 85 } 86 virtual void Write(byte data){ 87 //額外的加密操作... 88 stream::Write(data);//寫檔案流 89 //額外的加密操作... 90 } 91 }; 92 93 94 95 class BufferedStream : public DecoratorStream{ 96 97 Stream* stream;//... 98 99 public: 100 BufferedStream(Stream* stm):DecoratorStream(stm){ 101 102 } 103 //... 104 }; 105 106 void Process(){ 107 108 //執行時裝配 109 FileStream* s1=new FileStream(); 110 111 CryptoStream* s2=new CryptoStream(s1); 112 113 BufferedStream* s3=new BufferedStream(s1); 114 115 BufferedStream* s4=new BufferedStream(s2); 116 117 118 119 }decorator3.cpp
在DecoratorStream中,使用組合的方式實現多型,包含了Stream基類。
裝飾模式,動態地給一個物件新增一些額外的職責。就增加功能來說,裝飾模式相比生成子類更靈活。有時我們希望給某個物件而不是整個類新增一些功能。例如上面的例子,有一個檔案流,允許對它新增加密或者緩衝的擴充套件功能。裝飾模式下,會將檔案流嵌入到另一個物件(Decorator)中,由該物件完成特性的新增,該嵌入的物件就成為裝飾,這個裝飾與它所裝飾的元件介面一致,因此它對使用該元件的客戶透明。
動機
- 在某些情況下我們可能會“過度地使用繼承來擴充套件物件的功能”,由於繼承為型別引入的靜態特質,使得這種擴充套件方式缺乏靈活性;並且隨著子類的增多(擴充套件功能的增多),各種子類的組合(擴充套件功能的組合)會導致更多子類的膨脹。
- 如何使“物件功能的擴充套件”能夠根據需要來動態地實現?同時避免“擴充套件功能的增多”帶來的子類膨脹問題?從而使得任何“功能擴充套件變化”所導致的影響將為最低?
繼承引入的靜態特性如下第七行:
1 //擴充套件操作 2 class CryptoFileStream :public FileStream{ 3 public: 4 virtual char Read(int number){ 5 6 //額外的加密操作... 7 FileStream::Read(number);//讀檔案流 8 } 9 ... 10 }
組合引入的動態特性如下第八行:
1 class CryptoStream: public DecoratorStream { 2 3 public: 4 CryptoStream(Stream* stm):DecoratorStream(stm){ 5 6 } 7 8 virtual char Read(int number){ 9 //額外的加密操作... 10 stream->Read(number);//讀檔案流 11 } 12 ... 13 }
模式定義
動態(組合)地給一個物件增加一些額外的職責。就增加功能而言,Decorator模式比生成子類(繼承)更為靈活(消除重複程式碼 & 減少子類個數)。
——《設計模式》GoF
要點總結
- 通過採用組合而非繼承的手法, Decorator模式實現了在執行時動態擴充套件物件功能的能力,而且可以根據需要擴充套件多個功能。避免了使用繼承帶來的“靈活性差”和“多子類衍生問題”
- Decorator類在介面上表現為is-a Component的繼承關係,即Decorator類繼承了Component類所具有的介面。但在實現上又表現為has-a Component的組合關係,即Decorator類又使用了另外一個Component類
- Decorator模式的目的並非解決“多子類衍生的多繼承”問題,Decorator模式應用的要點在於解決“主體類在多個方向上的擴充套件功能”——是為“裝飾”的含義
(若一個類繼承了一個父類,且在欄位裡包含(組合)了該父類,很大可能就是Decorator設計模式,基類和組合在功能上由類似的地方,一般來說不會在一個類中同時繼承和組合同一個類。但在裝飾模式中,使用繼承是為了完善基類stream介面的規範,使用組合是為了支援呼叫filestream,netstream中的介面)
裝飾模式提供了更加靈活的向物件新增職責的方式。可以用新增和分離的方法,用裝飾在執行時刻增加和刪除職責。裝飾模式提供了一種“即用即付”的方法來新增職責。它並不試圖在一個複雜的可定製的類中支援所有可預見的特徵,相反,你可以定義一個簡單的類,並且用裝飾類給它逐漸地新增功能。可以從簡單的部件組合出複雜的功能