设计模式C++004__装饰器模式
设计模式C++004__装饰器模式
在软件组件设计中,如果职责划分不清晰,使用继承得到的结果往往会随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候关键是划清责任。
单一职责模式分类中的设计模式:
装饰器模式,
桥模式
1、装饰器模式:
动机:在某些情况下,我们可能会“过渡地使用继承来扩展对象的功能”,由于继承为类型引入静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各个子类的组合(扩展功能的组合)会导致更多子类的膨胀。
?如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响降低到最低?
装饰器模式的定义:
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)。--GoF
2、示例:
对文件流,内存流,网络流的,读文件,定位文件,写文件操作。
我们定义一个Stream流,抽象类。然后可以通过继承,获得子类文件流,网络流,内存流。
每种流都具有读流,定位流,写流的功能。实际开发当中每个子类中有1到3种方法。这样,排列组合下来,子类中的代码会急剧膨胀。
如果,我们再有需求变化,例如,我们需要对流进行写前加密,读后加密等等操作。我们可以继承各个2级子类,在这些3级子类中,每个读,写方法都要更改。
这种工作量是难以想象的,而且多处修改难免出错。
将来我们还有可能对这些流进行缓冲,怎么办,我们继续修改源代码?
使用继承来设计:
//业务操作
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();
}
类的关系图:
观察思考,发现,代码大量重复,每个子类基本逻辑和流程很像。
怎么办?答案是,不使用继承,使用组合。
以加密文件子类(CryptoFileStream )为例:
把子类定义代码中的:public FileStream,移动到子类的内部,使它成为一个子类中声明的一个实例变量 FileStream *fileStream;
使用继承来扩展加密文件流这一功能的子类
//扩展操作
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 CryptoFileStream {
FileStream *stream;
public:
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//读文件流,这里不是使用类作用域符号,而是使用运行时多态,即,指针来调用方法。
}
virtual void Seek(int position){
//额外的加密操作...
stream->Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream->Write(data);//写文件流
//额外的加密操作...
}
};
这样,其他几个流的加密功能子类,也可以如法炮制:
class CryptoNetworkStream {
NetworkStream *stream;
public:
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//读网络流
}
virtual void Seek(int position){
//额外的加密操作...
stream->Seek(position);//定位网络流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream->Write(data);//写网络流
//额外的加密操作...
}
};
class CryptoMemoryStream {
MemoryStream *stream;
public:
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//读内存流
}
virtual void Seek(int position){
//额外的加密操作...
stream->Seek(position);//定位内存流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream->Write(data);//写内存流
//额外的加密操作...
}
};
根据重构的原则,当一个类的多个子类被多次声明时,应该直接使用父类来声明来替代。这样就消除了代码重复性。
又由于这些文件中,有这些个虚函数是遵循Stream,规范的。所以 virtual char Read();这些个函数需要继承Stream;
这样,上面三个类直接可以这样声明:
//扩展操作
class CryptoFileStream : public Stream {
Stream *stream; //这里具体是什么,运行时决定例如 new FileStream();
//...
};
class CryptoNetworkStream : public Stream {
Stream *stream;//这里具体是什么,运行时决定例如 new NetworkStream();
};
class CryptoMemoryStream : public Stream {
Stream *stream;//这里具体是什么,运行时决定例如 new MemoryStream();
//...
};
我们发现上面三个类,又是重复的结构。 Stream *stream;在三个子类中被声明了3次。
我们可以直接把对各种流加密的这3个子类合为一个,也甭管是加密文件流,还是网络流,还是内存流了。类名取CryptoStream ,就代表,只对流加密。
class CryptoStream : public Stream {
Stream *stream; //这里具体是什么,运行时决定例如 new FileStream();
public:
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//读文件流,这里不是使用类作用域符号,而是使用运行时多态,即,指针来调用方法。
}
virtual void Seek(int position){
//额外的加密操作...
stream->Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream->Write(data);//写文件流
//额外的加密操作...
}
};
这也会有一个奇特的现象:通过组合替代继承会发现,CryptoStream 即继承了Stream类,里面同时又声明了一个Stream 类型的变量 stream;这是装饰器模式的典型特点。
class CryptoStream : public Stream {
Stream *stream;
此时把不需要的代码删除之后代码如下,版本2:
//业务操作
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 CryptoStream: public Stream {
Stream* stream;//...
public:
CryptoStream(Stream* stm):stream(stm){
}
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作...
stream::Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream::Write(data);//写文件流
//额外的加密操作...
}
};
class BufferedStream : public Stream{
Stream* stream;//...
public:
BufferedStream(Stream* stm):stream(stm){
}
//...
};
void Process(){
//运行时装配
FileStream* s1=new FileStream();
CryptoStream* s2=new CryptoStream(s1);
BufferedStream* s3=new BufferedStream(s1);
BufferedStream* s4=new BufferedStream(s2);
}
这时已经基本符合装饰器模式的思想了。
不过还可以再优化一下,就是通过继承Stream,延伸出来一个DecoratorStream类,这个类里面只有一个 protected Stream *stream;的声明。
三个对不同流操作的子类,定义为继承自DecoratorStream。
这样实际更规范一下,把三个子类中一样的声明提出来,单独作为一层。更符合重构的原则。
最终得到代码 v3:
//业务操作
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){
//写内存流
}
};
//扩展操作
DecoratorStream: public Stream{
protected:
Stream* stream;//...
DecoratorStream(Stream * stm):stream(stm){
}
};
class CryptoStream: public DecoratorStream {
public:
CryptoStream(Stream* stm):DecoratorStream(stm){
}
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作...
stream::Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream::Write(data);//写文件流
//额外的加密操作...
}
};
class BufferedStream : public DecoratorStream{
Stream* stream;//...
public:
BufferedStream(Stream* stm):DecoratorStream(stm){
}
//...
};
void Process(){
//运行时装配
FileStream* s1=new FileStream();
CryptoStream* s2=new CryptoStream(s1);
BufferedStream* s3=new BufferedStream(s1);
BufferedStream* s4=new BufferedStream(s2);
}
最终的类的继承关系如下
FileStream,NetworkStream,MemoryStream是三个原有的功能。我们要扩展出加密流和缓冲流的功能。这时候是在原有的类上添加新的功能。且原有的功能基本一致。
DecoratorStream继承自Stream, DecoratorStream中又有 一个Stream的变量声明。新的功能子类继承自这个奇怪有特别的类,运行时这个内部声明的指针就会指向具体的子类实例。
每次要新加功能,就只需要如法炮制。按照规范创建子类。
3、结构:
4、要点总结:
- 通过使用组合而非继承的手法,Decorator模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了使用继承带来的“灵活性差”和“多子类衍生问题”、
- Decorator类在接口上表现为is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。但在实现上有表现为has-a Component的组合关系,即Decorator类有使用了另外一个Component类。
- Decorator模式的目的并非解决“多子类衍生的多继承”问题,Decorator模式应用要点在于解决“主题类在多个方向上的扩展功能”--是为装饰的含义。
======自己当时的理解。
这样其他3个流也可以这样做: 。这里的关键点是,类中声明的这个父类指针,在编译时这个指针代表的东西都是一样的,甚至可以确定就是一个4字节的指针符号。运行时就不一样了,不同的子类会有各自不同的构造方法,也会指向对应的子类实例对象。
这样我们就打破了编译时依赖。利用C++多态特性,把这些依赖关系(组合到类中的原来的父类的实例变量)放到运行时确定。
这里注意到一个新的问题,消除了继承关系,也消除了很多冗余的子类中的代码,这些原来可以通过继承父类,得到的抽象接口我没有继承到,怎么办?
这也简单将父类的父类抽象出来,用子类继承就可以了。
这也会有一个奇特的现象:通过组合替代继承会发现
整体来看,使用组合+抽象接口消除了一层继承关系。这将极大缓解因为继承导致子类快速膨胀的问题。