第6節 裝飾者模式(單一職責)

一、單一職責模式概述

  1. 在軟件設計中,如果職責劃分不清晰,使用繼承得到的結果往往隨着需求的變化子類急劇膨脹,同時充斥着冗餘的代碼;
  2. 單一職責模式典型:裝飾者模式、橋模式

二、裝飾者模式動機

  1. 在某些情況下,我們可能會過度使用繼承來擴展對象的功能,由於繼承爲類型的靜態特性,使得這種擴展缺乏靈活性;
  2. 此外,隨着子類的增多,各種子類的組合會導致更多子類的膨脹;
  3. 如何使得對象功能的擴展能夠根據需要來動態實現?同時避免"擴展功能的增多"帶來的子類膨脹問題,從而使得任何功能擴展變化的影響降到最低?

三、程序示例

我們打算做一些IO操作,設計一些關於流的庫.

3.1 使用設計模式前

#include <stdio.h>

class Stream {                                        // 基類:定義流的一些基本操作
public:
    virtual char Read(int number)   = 0;              // 純虛函數
    virtual void Seek(int position) = 0;
    virtual void Write(char data)   = 0;
    virtual ~Stream() {};                             // 養成良好的習慣,基類析構函數寫成虛函數
};

// 主體類:文件流操作,繼承自Stream
class FileStream : public Stream {
public:
    virtual char Read(int number)  {/* code... */ }   // 讀取文件流
    virtual void Seek(int position){/* code... */ }   // 定位文件流
    virtual void Write(char data)  {/* code... */ }   // 寫入文件流
};

// 主體類:網絡流操作,繼承自Stream
class NetworkStream :public Stream {
public:
    virtual char Read(int number)   {/* code... */ }   // 讀取網絡流
    virtual void Seek(int position) {/* code... */ }   // 定位網絡流
    virtual void Write(char data)   {/* code... */ }   // 寫入網絡流
};

// 主體類:內存流操作,繼承自Stream
class MemoryStream :public Stream {
public:
    virtual char Read(int number)   {/* code... */ }   // 讀取內存流
    virtual void Seek(int position) {/* code... */ }   // 定位內存流
    virtual void Write(char data)   {/* code... */ }   // 寫入內存流
};

// 現在考慮擴展操作,如果我們要對文件流進行加密
class CryptoFileStream :public FileStream {
public:
    virtual char Read(int number) {                     // 讀取文件流
		//  額外的加密操作
		FileStream::Read(number);
    }  
    virtual void Seek(int position) {                   // 定位文件流
		//  額外的加密操作
		FileStream::Seek(position);
    }  
    virtual void Write(char data) {                     // 寫入文件流
		//  額外的加密操作
		FileStream::Write(data);
    }
};

// 針對網絡流的擴展
class CryptoNetworkStream :public NetworkStream {
public:
    virtual char Read(int number) {                     // 讀取網絡流
		//  額外的加密操作
		NetworkStream::Read(number);
    }
    virtual void Seek(int position) {                   // 定位網絡流
		//  額外的加密操作
		NetworkStream::Seek(position);
    }
    virtual void Write(char data) {                     // 寫入網絡流
		//  額外的加密操作
		NetworkStream::Write(data);
    }
};

class CryptoMemoryStream :public MemoryStream {
public:
    virtual char Read(int number) {                     // 讀取內存流
		//  額外的加密操作
		MemoryStream::Read(number);
    }
    virtual void Seek(int position) {                   // 定位內存流
		//  額外的加密操作
		MemoryStream::Seek(position);
    }
    virtual void Write(char data) {                     // 寫入內存流
		//  額外的加密操作
		MemoryStream::Write(data);
    }
};

// 繼續對FileStream進行擴展
class BufferedFileStream:public FileStream{ /* 額外的緩衝操作*/  };

// 對文件既有緩衝擴展又有加密擴展
class CryptoBufferedFileStream :public FileStream {     // 當然也可繼承自BufferedFileStream
	// 額外的緩衝操作
	// 額外的加密操作
};

int main()
{
    // 使用設計模式前,發生的是編譯時裝配
    CryptoFileStream   *fs1 = new CryptoFileStream();
    BufferedFileStream *fs2 = new BufferedFileStream();
    CryptoBufferedFileStream *fs3 = new CryptoBufferedFileStream();

    /*codes to do something... */
    delete fs3;
    delete fs2;
    delete fs1;
    renturn 0;
}

3.2 問題分析

使用設計模式前,代碼結構關係如下:

                                            Stream
	                                          ||
           FileStream                    NetworkStream                    MemoryStream
	          ||                              ||                               ||
        CryptoFileStream               CryptoNetworkStream             CryptoMemoryStream
       BufferedFileStream             BufferedNetworkStream           BufferedMemoryStream
    CryptoBufferedFileStream       CryptoBufferedNetworkStream      CryptoBufferedMemoryStream

假設擴展類型數量爲N,操作種類爲M,則在誰用設計模式前,類的總量爲O(MN),當M/N數量較多時,類數量是很大的;另一方面,我們發現類代碼裏有很多重複冗餘的操作.

我們是否能夠把O(MN) 變爲O(M+N)?在OOP設計原則裏,提到,能夠使用組合的方式我們儘量不適用繼承,使用組合方式後是否能講類數量變爲O(M+N)?

3.3 使用裝飾者模式

#include <stdio.h>

class Stream {                                        // 基類:定義流的一些基本操作
public:
    virtual char Read(int number)   = 0;
    virtual void Seek(int position) = 0;
    virtual void Write(char data)   = 0;
    virtual ~Stream() {};                             // 養成良好的習慣,基類析構函數寫成虛函數
};

// 主體類:文件流操作,繼承自Stream
class FileStream : public Stream {
public:
    virtual char Read(int number)  {/* code... */ }   // 讀取文件流
    virtual void Seek(int position){/* code... */ }   // 定位文件流
    virtual void Write(char data)  {/* code... */ }   // 寫入文件流
};

// 主體類:網絡流操作,繼承自Stream
class NetworkStream :public Stream {
public:
    virtual char Read(int number)   {/* code... */ }   // 讀取網絡流
    virtual void Seek(int position) {/* code... */ }   // 定位網絡流
    virtual void Write(char data)   {/* code... */ }   // 寫入網絡流
};

// 主體類:內存流操作,繼承自Stream
class MemoryStream :public Stream {
public:
    virtual char Read(int number)   {/* code... */ }   // 讀取內存流
    virtual void Seek(int position) {/* code... */ }   // 定位內存流
    virtual void Write(char data)   {/* code... */ }   // 寫入內存流
};

class CryptoStream{
public:
    CryptoStream(Stream* stream) { this->m_stream = stream; }
public:
    virtual char Read(int number) {                     
		doCrypto();
		m_stream->Read(number);
    }
    virtual void Seek(int position) {                  
		doCrypto();
		m_stream->Seek(position);
    }
    virtual void Write(char data) {                     
		doCrypto();
		m_stream->Write(data);
    }
private:
    void doCrypto() {}
private:
    Stream* m_stream;                    // 編譯時無關,運行時根據new的具體對象決定調用什麼方法
};

class BufferedStream{
public:
    BufferedStream(Stream* stream){ this->m_stream = stream; }      
public:
    virtual char Read(int number) {                    
		doBuffered();
		m_stream->Read(number);
    }
    virtual void Seek(int position) {                  
		doBuffered();
		m_stream->Seek(position);
    }
    virtual void Write(char data) {                    
		doBuffered();
		m_stream->Write(data);
    }
private:
    void doBuffered() {}
private:
    Stream* m_stream;                                  // 編譯時無關,運行時根據new的具體對象決定調用什麼方法
};    

int main()
{
    // 使用設計模式後,類的數量變爲加法關係,而不再是乘法關係(CryptoStream,BufferedStream)
    Stream         *s1 = new FileStream();
    CryptoStream   *s2 = new CryptoStream(s1);
    BufferedStream *s3 = new BufferedStream(s1);

    /*codes to do something... */
    delete s1; delete s2; delete s3;
    renturn 0;
}

3.4 總結分析

使用設計模式後,類的數量變爲加法關係,而非乘法關係(CryptoStream,BufferedStream).

四、再思考

如何實現既加密、又緩存?

  • 上述代碼s2和s3是相互分開的,相互直連沒有聯繫,怎麼實現交互? 他們之間的共同點是?自然是基類了
  • 因此我們不僅需要在類中聲明Stream對象,還需要繼承該類,而且需要實現基類的虛函數,才能new出一個對象,不實現基類虛方法是不能構造對象的.
  • 此時,應用代碼可以寫成:
#include <stdio.h>

class Stream {                                        // 基類:定義流的一些基本操作
public:
    virtual char Read(int number)   = 0;
    virtual void Seek(int position) = 0;
    virtual void Write(char data)   = 0;
    virtual ~Stream() {};                             // 養成良好的習慣,基類析構函數寫成虛函數
};

// 主體類:文件流操作,繼承自Stream
class FileStream : public Stream {
public:
    virtual char Read(int number)  {/* code... */ }   // 讀取文件流
    virtual void Seek(int position){/* code... */ }   // 定位文件流
    virtual void Write(char data)  {/* code... */ }   // 寫入文件流
};

// 主體類:網絡流操作,繼承自Stream
class NetworkStream :public Stream {
public:
    virtual char Read(int number)   {/* code... */ }   // 讀取網絡流
    virtual void Seek(int position) {/* code... */ }   // 定位網絡流
    virtual void Write(char data)   {/* code... */ }   // 寫入網絡流
};

// 主體類:內存流操作,繼承自Stream
class MemoryStream :public Stream {
public:
    virtual char Read(int number)   {/* code... */ }   // 讀取內存流
    virtual void Seek(int position) {/* code... */ }   // 定位內存流
    virtual void Write(char data)   {/* code... */ }   // 寫入內存流
};

class CryptoStream:public Stream{
public:
    CryptoStream(Stream* stream) { this->m_stream = stream; }
public:
    virtual char Read(int number) {                     
		doCrypto();
		m_stream->Read(number);
    }
    virtual void Seek(int position) {                  
		doCrypto();
		m_stream->Seek(position);
    }
    virtual void Write(char data) {                     
		doCrypto();
		m_stream->Write(data);
    }
private:
    void doCrypto() {}
private:
    Stream* m_stream;                    // 編譯時無關,運行時根據new的具體對象決定調用什麼方法
};

class BufferedStream:public Stream {
public:
    BufferedStream(Stream* stream){ this->m_stream = stream; }      
public:
    virtual char Read(int number) {                    
		doBuffered();
		m_stream->Read(number);
    }
    virtual void Seek(int position) {                  
		doBuffered();
		m_stream->Seek(position);
    }
    virtual void Write(char data) {                    
		doBuffered();
		m_stream->Write(data);
    }
private:
    void doBuffered() {}
private:
    Stream* m_stream;                    // 編譯時無關,運行時根據new的具體對象決定調用什麼方法
};    

int main()
{
    // 運行時裝配
    Stream         *m1 = new FileStream();
    CryptoStream   *m2 = new CryptoStream(m1);
    BufferedStream *m3 = new BufferedStream(m2);        // m3既可以加密,又可以緩存
    delete s1; delete s2; delete s3;

    /*codes to do something... */
    delete s1; delete s2; delete s3;
    renturn 0;
}

五、 再再思考

CryptoStream和BufferedStream含有公共的部分m_stream,根據GOF的思想,含有的公共部分可以網上提,即可以將 Stream* m_stream 放在基類裏,但是我們發現FileStream、NetworkStream、MemoryStream並不需要這個.由此,我們可以寫個中間類,比如MidStream,如文中代碼所述;此時,可以CryptoStream和BufferedStream讓繼承MidStream即可.

// 創建中間類,這裏不做虛函數實現,也就無法創建對象
class MidStream:public Stream {
protected:
	Stream *m_stream;
};

六、要點總結

  1. 通過採用組合而非繼承的手法,裝飾者模式實現了在運行時動態擴展對象功能的呢理工,而且可以根據需要擴展多個功能,避免了使用繼承帶來的"靈活性差"和"多子類衍生問題".
  2. 裝飾者類在接口上表現爲IS-A的繼承關係,但又在實現上表現爲Has-A的組合關係;
  3. 裝飾者模式的目的並非爲了解決"多子類衍生的多繼承"問題,而主要在於解決"主體類在多個方向上的擴展功能"——是爲"裝飾"的含義.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章