Decorator装饰模式

设计模式 2019-02-23 4741 字 294 浏览 点赞

前言

关于装饰模式我有以下几个问题想问:

  • 什么是装饰模式?
  • 为什么需要装饰模式(也就是什么情况下使用装饰模式)?
  • 装饰模式的优点是什么?
  • 装饰模式的缺点是什么?

我想通过回答上面四个问题,揭开装饰模式的面纱。

模式定义

在《设计模式》一书中,对装饰模式做了简洁而清晰的释义:

动态地给一个对象增加一些额外的职责。就功能而言,Decorator模式比继承更为灵活。

GoF既然用其跟继承做比较,说明这样的比较一定存在什么意义。

与继承比较

假设一个场景,我们需要设计Stream这样一个抽象基类,它包括了读写、定位操作:

class Stream {
public:
    virtual void read()=0;
    virtual void write()=0;
    virtual void seek()=0;
    
    ~Stream() {}
};

对现实中的流来说,存在符合的事物有很多,比如文件流,网络流,内存流……这些对“流”的操作都有读写、定位,也就是对抽象基类Stream实现。因此对它们的设计最好方式是继承自Stream:

class FileStream: public Stream {
public:
    // read,write,seek都是对文件流操作
    virtual void read() {...}
    virtual void write() {...}
    virtual void seek() {...}
};

class NetworkStream: public Stream {
public:
    // read,write,seek都是对网络流操作
    virtual void read() {...}
    virtual void write() {...}
    virtual void seek() {...}
};

class MemoryStream: public Stream {
public:
    // read,write,seek都是对内存流操作
    virtual void read() {...}
    virtual void write() {...}
    virtual void seek() {...}
}

由于FileStream,NetworkStream,MemoryStream分别对文件流,网络流,内存流操作,因此它们读写、定位操作都是不同的,所以不能在Stream中统一实现,需要在各自的类中定义。

现在,如果业务需求,需要对流做额外的如加密、缓存等功能,倘若沿用继承的方式,代码会成如下这样:

class CryptoFileStream: public FileStream {
public:
    virtual void read() {
        FileStream::read();
        // 解密操作
    }
    virtual void write() {
        // 加密操作
        FileStream::write();
    }
    virtual void seek() {
        // 解密操作
        FileStream::seek();
    }
}

class BufferFileStream: public FileStream {
public:
    ...
}

现在仅仅完成FileStream的加密和缓存操作,还有网络流和内存流没写,但它们同CryptoFileStream大体类似。这个类似是指,它们一定会调用父类的read()write()seek()等方法。更明确的说,对CryptoFileStream,一定会有FileStream::read();对CryptoNetworkStream,一定有NetworkStream::read(),对CryptoMemoryStream,一定有MemoryStream::read()

事实上,FileStream、NetworkStream、MemoryStream都继承自Stream。也就是说Stream类型的指针或者引用可以对它们统一管理,我们只要在程序运行时,说明stream表示的是FileStream呢,还是NetworkStream,或者MemoryStream就可以了。

因此代码修改可如下:

class CryptoFileStream: public FileStream {
private:
    Stream* stream;
public:
    CryptoFileStream(Stream* stm): stream(stm) {}
    virtual void read() {
        stream->read();
        // 解密操作
    }
    virtual void write() {
        // 加密操作
        stream->write();
    }
    virtual void seek() {
        // 解密操作
        stream->seek();
    }
};

int main()
{
    FileStream* fs = new FileStream();
    CryptoFileStream* cfs = new CryptoFileStream(fs);  // 动态的方式告诉stream指针指向谁
    ...
    return 0;
}

此时会发现,三种流的Crypto都有private: Stream* stream;,显然代码重复了。又因为它们有相同的祖先,是不是意味着可以在祖先类Stream中加上这行语句呢?

——别忙下结论!作为Stream的直接子类FileStream,NetworkStream,MemoryStream都不需要stream指针,所以直接在Stream类中添加,看起来不像是明智的行为。继承不够灵活在这里体现。

装饰模式

事实上,我们就是想给Crypto*类添加一个Stream类型的指针而已——这个“添加”,正在接近“装饰”的意思了。根据上面最后改进得到的代码,已经可以让CryptoFileStream、CryptoNetworkStream、CryptoMemoryStream脱离FileStream、NetworkStream和MemoryStream的掌控。因为尽管我们用到了父类的方法,但并非通过继承的方式使用,而是组合,这是指针stream起到的功效。所以Crypto*类没必要分别继承不同的类,它们只要继承自同一个含有stream字段的类,不就是都有stream指针了吗?

现在,为这个类取名叫DecoratorStream——是的,它是这个装饰模式中的关键部分,它叫抽象装饰类

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

此后,Crypto*类得到救赎——CryptoFileStream、CryptoNetworkStream、CryptoMemoryStream可以合并为一个类:CryptoStream。(当然,这是在三种流加密解密算法一样的情况下,如果不一样,我认为合并为一个类并非最好方式)

class CryptoStream: public DecoratorStream {
public:
    CryptoStream(stream* stm): DecoratorStream(stm) {}
    virtual void read() {
        stream->read();
        // 解密操作
    }
    virtual void write() {
        // 加密操作
        stream->write();
    }
    virtual void seek() {
        // 解密操作
        stream->seek();
    }
}

注意DecoratorStream类,它继承了Stream(继承是为了让它的派生类遵从Stream的接口设计),又组合了Stream(组合是为了动态的使用FileStream等一系列类的功能)。当看到一个类有这样的行为时,这个类多半就是装饰类了,其设计模式很大概率是装饰模式。

关系梳理

如果一味的继承,那么类与类的关系呈现如下:

如果使用装饰模式,那么类与类关系如下:

可以看到,子类的数量也得到了有效控制。

模式结构

装饰模式包含如下角色:

  • Component: 抽象构件
  • ConcreteComponent: 具体构件
  • Decorator: 抽象装饰类
  • ConcreteDecorator: 具体装饰类

总结

装饰模式的优点:

  • 装饰模式可以比继承关系更灵活的扩展扩展对象功能;
  • 使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象;
  • 具体构件类(上述中的FileStream、NetworkStream)与具体装饰类(上述中的CryptoStream、BufferStream)能够独立变化。用户可以根据需要增加新的具体构件类和具体装饰类,使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。

装饰模式的缺点:

  • 装饰模式灵活的特点,也意味着加大了出错时对其排查问题的困难度。

尽管装饰模式避免了继承关系带来的“多子类衍生问题”,但它的目的并非于此,其旨在:解决“主体类在多个方向上扩展功能”。

感谢

  • 参考李建忠老师的《C++模式设计》
  • 参考《Graphic Design Patterns》中的装饰模式


本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论