以下内容均来自GeekBand极客班C++ 设计模式课程(李建忠老师主讲)
参考文献见:https://download.csdn.net/download/qq_41605114/18881763
Decorator
“单一职责”模式:
在软件组件的设计中,如果责任划分不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码(典型的Bad smell),这时候的关键是划分责任。
典型模式:
Decorator
Bridge
动机(Motivation)
在某些情况下,我们可能会“过度地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增加),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响降到最低。
《设计模式》GOF的定义:
“动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)”
蓝色为稳定部分,橘黄色为变化部分
下面我们以一个流操作为案例:
结构化编程
流的主体如下:
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 CrytoFileStream : 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 CrytoNetworkStream : public NetworkStream{
};
class CrytoMemoryStream : public MemoryStream{
};
增加缓冲区:
//增加缓冲区
class BufferedFileStream : public FileStream{
};
class BufferedNetworkStream : public NetworkStream{
};
class BufferedMemoryStream : public MemoryStream{
};
现在对上述的继承关系进行梳理,树状图如下:
从上图可知,如果继续增加需求,那么会导致子类不断膨胀,此时显然是要进行优化,划分清楚责任
为了优化整个结构,我们首先对继承部分进行优化:
利用组合替代继承:
class CrytoFileStream{
FileStream * stream;
public:
virtual char Read(int number){
//额外的加密操作
stream->Read(number);//直接用父类的,此处必须声明作用域
//...读文件流
}
virtual void Seek(int position){
//额外的加密操作
stream->Seek(position);//直接用父类的,此处必须声明作用域
//...定位文件流
}
virtual void Write(char data){
//额外的加密操作
stream->Write(data);//直接用父类的,此处必须声明作用域
//...写文件流
}
};
对比一下代码段:
继承 : 组合:
我们可以看到,使用继承的CrytoFileStream class中的FileStream::Read/Seek/Write部分,全部是静态特质,是无法进行扩展的,只能进行改变。
直接替换成使用组合的CrytoFileStream class
下面我们对所有的扩展内容都进行同样的操作:
class CrytoFileStream{
FileStream * stream;
public:
virtual char Read(int number){
//额外的加密操作
stream->Read(number);//直接用父类的,此处必须声明作用域
//...读文件流
}
};
class CrytoNetworkStream{
NetworkStream * stream;
public:
virtual char Read(int number){
//额外的加密操作
stream->Read(number);//直接用父类的,此处必须声明作用域
//...读文件流
}
};
class CrytoMemoryStream{
MemoryStream * stream;
public:
virtual char Read(int number){
//额外的加密操作
stream->Read(number);//直接用父类的,此处必须声明作用域
//...读文件流
}
};
可以看到,上面的代码更加接近了,加密扩展到的三个类几乎是在完成相同的操作
我们直接把FileStream,NetworkStream,MemoryStream的对象Stream完全可以使用多态进行代替
直接
将上面三个类合为一个类
class CrytoStream : public Stream{
Stream * stream;
public:
virtual char Read(int number){
//额外的加密操作
stream->Read(number);//直接用父类的,此处必须声明作用域
//...读文件流
}
virtual void Seek(int position){
//额外的加密操作
stream->Seek(position);//直接用父类的,此处必须声明作用域
//...定位文件流
}
virtual void Write(char data){
//额外的加密操作
stream->Write(data);//直接用父类的,此处必须声明作用域
//...写文件流
}
};
同理,Buffer类也可以合为一个类
//增加缓冲区
class BufferedStream : public Stream{
Stream * stream;
public:
BufferedStream(Stream * arg):stream(arg){
}
virtual char Read(int number){
//额外的缓冲操作
stream->Read(number);//直接用父类的,此处必须声明作用域
//...读文件流
}
virtual void Seek(int position){
//额外的缓冲操作
stream->Seek(position);//直接用父类的,此处必须声明作用域
//...定位文件流
}
virtual void Write(char data){
//额外的缓冲操作
stream->Write(data);//直接用父类的,此处必须声明作用域
//...写文件流
}
};
从CrytoStream类和bufferedStream类中可以看出,其中含有相同的字段,那么按照重构的理论,要么把这些字段提取出来放在父类中,要么就做一个中间类
放在父类中的话,FileStream不需要这个字段,所以没有任何的必要
那么现在做一个中间类 DecoratorStream类,具体操作如下:
class DecoratorStream : public Stream{
protected:
Stream * stream;
public:
DecoratorStream(Stream * arg):stream(arg){
}
};
class CrytoStream : public DecoratorStream{
public:
CrytoStream(Stream * arg):DecoratorStream(arg){
}
virtual char Read(int number){
//额外的加密操作
stream->Read(number);//直接用父类的,此处必须声明作用域
//...读文件流
}
virtual void Seek(int position){
//额外的加密操作
stream->Seek(position);//直接用父类的,此处必须声明作用域
//...定位文件流
}
virtual void Write(char data){
//额外的加密操作
stream->Write(data);//直接用父类的,此处必须声明作用域
//...写文件流
}
};
//增加缓冲区
class BufferedStream : public DecoratorStream{
public:
BufferedStream(Stream * arg):DecoratorStream(arg){
}
virtual char Read(int number){
//额外的缓冲操作
stream->Read(number);//直接用父类的,此处必须声明作用域
//...读文件流
}
virtual void Seek(int position){
//额外的缓冲操作
stream->Seek(position);//直接用父类的,此处必须声明作用域
//...定位文件流
}
virtual void Write(char data){
//额外的缓冲操作
stream->Write(data);//直接用父类的,此处必须声明作用域
//...写文件流
}
};
加上最开始的抽象类和主体类
//Decorator
class Stream{
public:
virtual char Read(int number) = 0;
virtual char Seek(int position) = 0;
virtual char Write(char data) = 0;
virtual ~Stream(){}
};
//主体类
class FileStream : public Stream{
public:
virtual char Read(int number){
//...读文件流
}
virtual char Seek(int position){
//...定位文件流
}
virtual char 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){
//...写内存流
}
};
下面我们可以看出,树状结构转化为了下面的形式
要点总结
1,通过采用组合而非继承的手法,Decorator模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了使用继承带来的“灵活性差”和“多子类衍生问题”。
2,DecoratorStream类在接口上表现为is-a Stream的继承关系,即DecoratorStream类继承了Stream类所具有的接口。但是实际上又表现为has-a Stream的组合继承关系,及DecoratorStream类又使用了另外一个Stream。
3,DecoratorStream模式的目的并非解决“多子类衍生的多继承”问题,DecoratorStream模式应用的要点在于解决“主体类在多个方向上的扩展功能”——是为“装饰”的含义。