前言
关于装饰模式我有以下几个问题想问:
- 什么是装饰模式?
- 为什么需要装饰模式(也就是什么情况下使用装饰模式)?
- 装饰模式的优点是什么?
- 装饰模式的缺点是什么?
我想通过回答上面四个问题,揭开装饰模式的面纱。
模式定义
在《设计模式》一书中,对装饰模式做了简洁而清晰的释义:
动态地给一个对象增加一些额外的职责。就功能而言,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》中的装饰模式篇
还不快抢沙发