C++设计模式 #5 装饰模式(Decorator)

发布时间:2023年12月23日

“单一职责”模式

  • 在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时又充斥着重复代码,这时候的关键是划清责任。

动机

  • 在某些情况下,我们可能会“过度的使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。

如何使“对象功能的扩展”能够根据需要来动态的实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响降为最低?

举个栗子

我们在软件构建的过程中,可能会遇到很多针对“流”的操作。比如文件流,网络流等等。很自然的,我们将“流”这一概念抽象,形成一个基类,我们可能得到如下的代码

//流的操作
class Stream{
public:
    virtual string Read(int number) = 0;
    virtual void Seek(int position) = 0;
    virtual void Write(string data) = 0;

    virtual ~Stream(){}
};

//功能实现的主体类
//文件流
class FileStream : public Stream{
private:
    //...
public:
    virtual string Read(int number){
        //读取某个编号的文件流
    }

    virtual void Seek(int position){
        //定位某个编号的文件流
    }

    virtual void Write(string data){
        //写文件流
    }
};

//网络流
class NetworkStream : public Stream{
private:
    //...
public:
    virtual string Read(int number){
        //读取某个网络流
    }

    virtual void Seek(int position){
        //定位某个网络流
    }

    virtual void Write(string data){
        //写网络流
    }
};

很多情况下,我们需要对这些类进行一些扩展操作,比如对其进行加密,有可能的操作如下:

class CryptoFileStream : public FileStream{
public:
    virtual string Read(int number){
        //解密操作
        FileStream::Read(number);
    }

    virtual string Seek(int position){
        //加密操作
        FileStream::Seek(position);
        //加密操作
    }

    virtual string Write(string data){
        //加密操作
        FileStream::Write(data);
        //加密操作
    }
};

class CryptoNetworkStream : public NetwordStream{
public:
    virtual string Read(int number){
        //解密操作
        NetwordStream::Read(number);
    }

    virtual string Seek(int position){
        //加密操作
        NetwordStream::Seek(position);
        //加密操作
    }

    virtual string Write(string data){
        //加密操作
        NetwordStream::Write(data);
        //加密操作
    }
};

不仅如此,我们还有很大可能有其他需求,例如,对一个流进行缓冲(buffer),对一个流进行部分解密并且转发。。等等

存在的问题

我们观察上面图中的情况,在需求增加后,类的个数由于一昧的继承关系,会有爆炸式的增长。再结合代码分析,如果我们使用同一种对数据的加密方式,我们在CryptoFileStream 类和CryptoNetworkStream 类中,写了高度重复的代码,也就是对数据的加密过程。

C++设计模式#1-CSDN博客我们的设计原则中有一条,叫做优先使用对象组合,而不是类继承

我们思考一下,FileStream/NetworkStream是确实继承于Stream的。但是,CryptoNetworkStream类是必须继承NetworkStream类的吗?这其实是一个功能上的扩展,而不是一个真正的非继承不可的关系。

重构

class CryptoStream : public Stream{
private:
    Stream* m_stream;
public:
    CryptoStream(Stream* stream) : m_stream(stream) {}

    virtual string Read(int number){
        //解密操作
        m_stream->Read(number);
    }

    virtual string Seek(int position){
        //加密操作
        m_stream->Seek(position);
        //加密操作
    }

    virtual string Write(string data){
        //加密操作
        m_stream->Write(data);
        //加密操作
    }
};


int main(){

    FileStream* f1 = new FileStream();
    NetworkStream* n1 = new NetworkStream();

    
    CryptoStream cs1(f1);
    CryptoStream cs2(n1);

    cs1.Read(number);
    cs2.Write(data);
    
    //...
}

我们观察CryptoStream 类,不再从FileStream或其他流的子类中继承,而是将其作为一种对象组合。在运行时对其进行装配,充分的利用了多态的技术,避免了为每一个流的子类都重写一个加密的子类这种冗余的情况。

其中,从Stream类继承是因为要保证read,seek,write三种方法的接口规范。另外,如果有其他例如缓冲的需求,可以用同样的方式实现。

进一步重构

我们提到了,如果有一个缓冲的需求,我们可以再用同样的方法实现。例如实现一个BufferStream 类。那么,我们在脑海中构建一个这样的BufferStream类,我们发现,这个类也是从Stream类中继承而来,同样也有一个Stream*的指针成员变量。

在设计原则中,如果一个类的多个子类有相同的结构,那么应该将这些相同的部分向上提。

那么,试想一下,如果我们将Stream*这个指针提到Stream类中是否合适。很明显,这是不合适的,因为FileStream也是从Stream中继承的,但是明显它不需要这样一个指针成员。

所以,应该设计一个中间类,也就是Decorator

class DecoratorStream : public Stream{
protected:
    Stream* stream;
public:
    DecoratorStream(Stream* stm) : stream(stm) {}
};

class CryptoStream : public DecoratorStream{
public:
    CryptoStream(Stream* stream) : DecoratorStream(stream) {}

    virtual string Read(int number){
        //解密操作
        stream->Read(number);
    }

    virtual string Seek(int position){
        //加密操作
        stream->Seek(position);
        //加密操作
    }

    virtual string Write(string data){
        //加密操作
        stream->Write(data);
        //加密操作
    }
};

如果有bufferstream类可以用同样的方式实现。

现在我们的类的规模变成了上图中的形式。我们成功的将一种乘法的关系变成了加法的关系。避免了这种爆炸式的类规模的增长。我们用一种“组合”的方式,解构了继承的关系。

模式定义

动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,装饰模式比生成子类(继承)更为灵活(消除重复代码,并且减少子类个数)。——《设计模式》GoF

Component -> Stream

Decorator -> DecoratorStream

ConcreteComponent -> FileStream/NetworkStream

ConcreteDecoratorA/B ->?CryptoStream/BufferStream

总结

  • 通过采用组合而非继承的方法,装饰模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了继承带来的“灵活性差”和“多子类衍生问题”。
  • 装饰模式在接口上表现为is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。但在实现上又表现为has-a Component的组合关系,即Decorator类又使用了一个Component类。(继承了一个类(继承),又有一个这个类的成员(组合)
  • 装饰模式的目的并非解决“多子类衍生的多继承”问题,装饰模式的应用的要点在于解决“主体类在多个方向上的扩展功能”,这就是装饰的含义。
文章来源:https://blog.csdn.net/A11en3/article/details/135160575
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。