一、單一職責模式概述
- 在軟件設計中,如果職責劃分不清晰,使用繼承得到的結果往往隨着需求的變化子類急劇膨脹,同時充斥着冗餘的代碼;
- 單一職責模式典型:裝飾者模式、橋模式
二、裝飾者模式動機
- 在某些情況下,我們可能會過度使用繼承來擴展對象的功能,由於繼承爲類型的靜態特性,使得這種擴展缺乏靈活性;
- 此外,隨着子類的增多,各種子類的組合會導致更多子類的膨脹;
- 如何使得對象功能的擴展能夠根據需要來動態實現?同時避免"擴展功能的增多"帶來的子類膨脹問題,從而使得任何功能擴展變化的影響降到最低?
三、程序示例
我們打算做一些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;
};
六、要點總結
- 通過採用組合而非繼承的手法,裝飾者模式實現了在運行時動態擴展對象功能的呢理工,而且可以根據需要擴展多個功能,避免了使用繼承帶來的"靈活性差"和"多子類衍生問題".
- 裝飾者類在接口上表現爲IS-A的繼承關係,但又在實現上表現爲Has-A的組合關係;
- 裝飾者模式的目的並非爲了解決"多子類衍生的多繼承"問題,而主要在於解決"主體類在多個方向上的擴展功能"——是爲"裝飾"的含義.