设计模式7-装饰模式
- 写在前面
- 动机
- 模式定义
- 结构
- 代码推导
- 原始代码
- 解决
- 问题分析
- 选择装饰模式的理由
- 1. 职责分离(Single Responsibility Principle)
- 2. 动态扩展功能
- 3. 避免类爆炸
- 4. 开闭原则(Open/Closed Principle)
- 5. 更好的组合复用
- 例子对比
- 详细说明
- 1. 基本流接口 `Stream`
- 2. 文件流实现 `FileStream`
- 3. 装饰器基类 `StreamDecorator`
- 4. 加密装饰器 `CryptoStream`
- 5. 缓冲装饰器 `BufferedStream`
- 6. 使用示例 `Process`
- 总结
- 要点总结
写在前面
单一职责模式:
-
在软件组件的设计中,如果责任划分的不清晰,使用记者得到的结果往往是跟随需求的变化,以及子类的增加而急剧膨胀。同时充值的重复代码。这个时候就应该责任划分清楚。使每个类负责自己的责任模块。这才是单一职责模式的关键。
-
典型模式:装饰模式(decorator model),桥模式(Bridge model)
动机
-
在某些情况下,我们可能会过多的使用技巧来扩展对象的功能。由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性。随着子类的增多,也就是扩展功能增多。各种子类的组合(扩展功能组合)。
-
那么如何使对象功能的扩展能够根据需要来动态的实现?而且同时避免扩展功能增多带来的子类膨胀问题。从而使得任何功能扩展变化所导致的影响降为最低。这就是装饰模式的目的。
模式定义
动态组合的给一个对象增加一些额外的职责,就增加工人而言,装饰模式比生成子类更加灵活。也就是消除重复代码以及减少子类个数。
结构
代码推导
原始代码
//业务操作
class Stream{
public:
virtual char Read(int number)=0;
virtual void Seek(int position)=0;
virtual void Write(char data)=0;
virtual ~Stream(){}
};
//主体类
class FileStream: public Stream{
public:
virtual char Read(int number){
//读文件流
}
virtual void Seek(int position){
//定位文件流
}
virtual void Write(char data){
//写文件流
}
};
class NetworkStream :public Stream{
public:
virtual char Read(int number){
//读网络流
}
virtual void Seek(int position){
//定位网络流
}
virtual void Write(char data){
//写网络流
}
};
class MemoryStream :public Stream{
public:
virtual char Read(int number){
//读内存流
}
virtual void Seek(int position){
//定位内存流
}
virtual void Write(char data){
//写内存流
}
};
//扩展操作
class CryptoFileStream :public FileStream{
public:
virtual char Read(int number){
//额外的加密操作...
FileStream::Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作...
FileStream::Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte 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(byte 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(byte data){
//额外的加密操作...
MemoryStream::Write(data);//写内存流
//额外的加密操作...
}
};
class BufferedFileStream : public FileStream{
//...
};
class BufferedNetworkStream : public NetworkStream{
//...
};
class BufferedMemoryStream : public MemoryStream{
//...
}
class CryptoBufferedFileStream :public FileStream{
public:
virtual char Read(int number){
//额外的加密操作...
//额外的缓冲操作...
FileStream::Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作...
//额外的缓冲操作...
FileStream::Seek(position);//定位文件流
//额外的加密操作...
//额外的缓冲操作...
}
virtual void Write(byte data){
//额外的加密操作...
//额外的缓冲操作...
FileStream::Write(data);//写文件流
//额外的加密操作...
//额外的缓冲操作...
}
};
void Process(){
//编译时装配
CryptoFileStream *fs1 = new CryptoFileStream();
BufferedFileStream *fs2 = new BufferedFileStream();
CryptoBufferedFileStream *fs3 =new CryptoBufferedFileStream();
}
这段代码存在几个缺陷。主要包括类的继承结构复杂,重复代码多,扩展性差等问题。
缺陷分析:
从上述图像可以看出,按照继承模式去写子类。那么子类的数量将会是1个抽象stream类 ,n(此时n=3)个基础子类,n*m个扩展子类,子类与子类间功能还会交叉。
1.类结构复杂。
- 类继承关系较为复杂,多个类之间存在多重继承(如CryptoFileStream和BufferedFileStream),且同一类的不同变种都要各自继承主类(FileStream)。
- 这种设计导致继承树较为复杂,难以维护和扩展。
2.重复代码多
- CryptoFileStream、CryptoNetworkStream、CryptoMemoryStream中的加密操作代码几乎相同。
- BufferedFileStream、BufferedNetworkStream、BufferedMemoryStream中的缓冲操作代码也基本一致。
3.扩展性差
- 如果需要增加新的功能,如压缩操作,需要再新增大量类似的类(如CryptoBufferedCompressedFileStream等),导致类的数量急剧增加,扩展性极差。
解决
那么按照继承的逻辑将加密功能以虚函数的形式写入到基类中,有需要重写加密函数不就行了?
将加密功能作为虚函数写入基类的确是一种方式,但是这种方法会有一些缺点,特别是在职责分离和代码扩展性方面。
问题分析
1. 职责不单一:
- 如果将加密功能直接写入基类,那么基类需要承担多种职责:基本流操作(读、写、定位)和加密操作。这违反了单一职责原则(SRP),即一个类应该只有一个引起其变化的原因。
2. 代码复杂度增加:
- 基类中加入加密相关的虚函数后,所有子类都需要考虑加密操作,即使某些子类并不需要加密功能。这会增加代码的复杂度。
3. 扩展性差:
- 如果未来需要添加新的功能(例如压缩、日志记录等),那么基类将会变得越来越臃肿,不同子类需要覆盖不同的虚函数,扩展性差。
选择装饰模式的理由
选择装饰器模式的理由主要有以下几点:
1. 职责分离(Single Responsibility Principle)
装饰器模式允许将不同的功能(如加密、缓冲)分离到不同的类中,从而使每个类只负责一种职责。这符合单一职责原则(Single Responsibility Principle),使代码更易于理解、维护和扩展。
2. 动态扩展功能
装饰器模式可以在运行时动态地组合对象,添加或移除功能,而不需要修改对象本身。这提供了比继承更灵活的功能扩展方式。例如,可以动态地对一个流对象添加加密功能、缓冲功能,甚至多个功能的组合。
3. 避免类爆炸
如果通过继承来实现功能扩展,每种功能组合都需要一个新的子类,类的数量会迅速增加,导致类爆炸问题。装饰器模式通过将功能封装在独立的装饰器类中,避免了大量的子类定义。
4. 开闭原则(Open/Closed Principle)
装饰器模式使类对扩展开放,对修改关闭。可以通过添加新的装饰器类来扩展功能,而无需修改已有的类。这符合开闭原则,提高了代码的可扩展性和灵活性。
5. 更好的组合复用
装饰器模式允许通过不同的装饰器类进行自由组合,复用功能模块。例如,可以同时添加加密和缓冲功能,而无需创建一个专门的“加密缓冲流”类。
例子对比
继承方式的缺点:
class Stream {
public:
virtual char Read(int number) = 0;
virtual void Seek(int position) = 0;
virtual void Write(char data) = 0;
virtual ~Stream() {}
};
class FileStream : public Stream {
public:
virtual char Read(int number) {
// 读文件流
}
virtual void Seek(int position) {
// 定位文件流
}
virtual void Write(char data) {
// 写文件流
}
};
// 如果需要加密和缓冲功能,需要创建多个类
class CryptoFileStream : public FileStream {
// 实现加密功能
};
class BufferedFileStream : public FileStream {
// 实现缓冲功能
};
class CryptoBufferedFileStream : public FileStream {
// 实现加密和缓冲功能
};
装饰器模式的优点:
class Stream {
public:
virtual char Read(int number) = 0;
virtual void Seek(int position) = 0;
virtual void Write(char data) = 0;
virtual ~Stream() {}
};
class FileStream : public Stream {
public:
virtual char Read(int number) {
// 读文件流
}
virtual void Seek(int position) {
// 定位文件流
}
virtual void Write(char data) {
// 写文件流
}
};
// 装饰器基类
class StreamDecorator : public Stream {
protected:
Stream* stream;
public:
StreamDecorator(Stream* strm) : stream(strm) {}
virtual char Read(int number) {
return stream->Read(number);
}
virtual void Seek(int position) {
stream->Seek(position);
}
virtual void Write(char data) {
stream->Write(data);
}
};
// 加密装饰器
class CryptoStream : public StreamDecorator {
public:
CryptoStream(Stream* strm) : StreamDecorator(strm) {}
virtual char Read(int number) {
// 额外的加密操作...
return StreamDecorator::Read(number);
}
virtual void Seek(int position) {
// 额外的加密操作...
StreamDecorator::Seek(position);
// 额外的加密操作...
}
virtual void Write(char data) {
// 额外的加密操作...
StreamDecorator::Write(data);
// 额外的加密操作...
}
};
// 缓冲装饰器
class BufferedStream : public StreamDecorator {
public:
BufferedStream(Stream* strm) : StreamDecorator(strm) {}
virtual char Read(int number) {
// 额外的缓冲操作...
return StreamDecorator::Read(number);
}
virtual void Seek(int position) {
// 额外的缓冲操作...
StreamDecorator::Seek(position);
// 额外的缓冲操作...
}
virtual void Write(char data) {
// 额外的缓冲操作...
StreamDecorator::Write(data);
// 额外的缓冲操作...
}
};
// 使用示例
void Process() {
Stream* fileStream = new FileStream();
Stream* cryptoStream = new CryptoStream(fileStream);
Stream* bufferedCryptoStream = new BufferedStream(cryptoStream);
bufferedCryptoStream->Read(100);
bufferedCryptoStream->Write('A');
bufferedCryptoStream->Seek(10);
delete bufferedCryptoStream; // 注意:需要确保正确删除链上的所有装饰器
delete cryptoStream;
delete fileStream;
}
详细说明
这段代码演示了如何使用装饰器模式来动态地为基本的文件流添加加密和缓冲功能。以下是代码的详细说明:
1. 基本流接口 Stream
Stream
是一个抽象基类,定义了三个纯虚函数:Read
、Seek
和 Write
。所有具体的流类都必须实现这些函数。
class Stream {
public:
virtual char Read(int number) = 0;
virtual void Seek(int position) = 0;
virtual void Write(char data) = 0;
virtual ~Stream() {}
};
2. 文件流实现 FileStream
FileStream
是一个具体的流类,继承自 Stream
并实现了 Read
、Seek
和 Write
方法。具体的实现细节在代码中没有给出,但假设它们涉及对文件的读写操作。
class FileStream : public Stream {
public:
virtual char Read(int number) {
// 读文件流
}
virtual void Seek(int position) {
// 定位文件流
}
virtual void Write(char data) {
// 写文件流
}
};
3. 装饰器基类 StreamDecorator
StreamDecorator
继承自 Stream
,并持有一个指向 Stream
对象的指针 stream
。StreamDecorator
通过调用 stream
指针的相应方法实现 Read
、Seek
和 Write
,从而将所有操作委托给被装饰的流对象。
class StreamDecorator : public Stream {
protected:
Stream* stream;
public:
StreamDecorator(Stream* strm) : stream(strm) {}
virtual char Read(int number) {
return stream->Read(number);
}
virtual void Seek(int position) {
stream->Seek(position);
}
virtual void Write(char data) {
stream->Write(data);
}
};
4. 加密装饰器 CryptoStream
CryptoStream
继承自 StreamDecorator
,并在调用 StreamDecorator
的 Read
、Seek
和 Write
方法之前或之后添加额外的加密操作。
class CryptoStream : public StreamDecorator {
public:
CryptoStream(Stream* strm) : StreamDecorator(strm) {}
virtual char Read(int number) {
// 额外的加密操作...
return StreamDecorator::Read(number);
}
virtual void Seek(int position) {
// 额外的加密操作...
StreamDecorator::Seek(position);
// 额外的加密操作...
}
virtual void Write(char data) {
// 额外的加密操作...
StreamDecorator::Write(data);
// 额外的加密操作...
}
};
5. 缓冲装饰器 BufferedStream
BufferedStream
继承自 StreamDecorator
,并在调用 StreamDecorator
的 Read
、Seek
和 Write
方法之前或之后添加额外的缓冲操作。
class BufferedStream : public StreamDecorator {
public:
BufferedStream(Stream* strm) : StreamDecorator(strm) {}
virtual char Read(int number) {
// 额外的缓冲操作...
return StreamDecorator::Read(number);
}
virtual void Seek(int position) {
// 额外的缓冲操作...
StreamDecorator::Seek(position);
// 额外的缓冲操作...
}
virtual void Write(char data) {
// 额外的缓冲操作...
StreamDecorator::Write(data);
// 额外的缓冲操作...
}
};
6. 使用示例 Process
在 Process
函数中,首先创建一个 FileStream
对象,然后将其装饰为 CryptoStream
和 BufferedStream
,最终形成一个既具有加密功能又具有缓冲功能的流对象。
void Process() {
Stream* fileStream = new FileStream(); // 基本的文件流
Stream* cryptoStream = new CryptoStream(fileStream); // 加密装饰
Stream* bufferedCryptoStream = new BufferedStream(cryptoStream); // 缓冲装饰
bufferedCryptoStream->Read(100); // 读取操作,包含加密和缓冲
bufferedCryptoStream->Write('A'); // 写入操作,包含加密和缓冲
bufferedCryptoStream->Seek(10); // 定位操作,包含加密和缓冲
delete bufferedCryptoStream; // 注意:需要确保正确删除链上的所有装饰器
delete cryptoStream;
delete fileStream;
}
总结
- 职责分离:每个装饰器类(如
CryptoStream
和BufferedStream
)只负责增加一个特定的功能,使得代码更符合单一职责原则。 - 动态组合:通过层层包装,可以在运行时动态地为基本流对象添加多种功能(如加密和缓冲),提高了代码的灵活性和可扩展性。
- 代码复用:装饰器类可以复用,避免了为每个功能组合创建大量子类的情况。
从上图可以看出此时类的结构为1+n+1+m。一个抽象类加上n个基础类,加上一个装饰类。再加上m个功能类。对比于修改之前类的层次。冗余的代码量大大减少同时也减少了子类的数量。
要点总结
- 通过采用组合而非计件的方法,装饰模式实现了在运行时动态扩展对象功能的能力。而且可以根据需要扩展多功能,避免了使用继承带来的灵活性差和子类衍生问题。
- 装饰器类在接口上表现为一个组合的继承关系。装饰器类继承了组合类所具有的接口但是在实现上又表现为组合关系。
- 装饰器模式的目的并非解决多子类衍生的多继承问题。装饰器模式应用的要点在于解决主体内在多个方向上的扩展功能。就是装饰的含义。