设计模式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++多态特性,把这些依赖关系(组合到类中的原来的父类的实例变量)放到运行时确定。

这里注意到一个新的问题,消除了继承关系,也消除了很多冗余的子类中的代码,这些原来可以通过继承父类,得到的抽象接口我没有继承到,怎么办?
这也简单将父类的父类抽象出来,用子类继承就可以了。
这也会有一个奇特的现象:通过组合替代继承会发现

整体来看,使用组合+抽象接口消除了一层继承关系。这将极大缓解因为继承导致子类快速膨胀的问题。
posted @ 2024-07-25 19:14  wjwdive  阅读(17)  评论(0编辑  收藏  举报