装饰器模式(Decorator)
由李建忠老师设计模式课程整理。
装饰器类的应用要点在于解决主体类在多个方向上的扩展功能。
GoF《设计模式》中的定义:
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码 &和减少子类个数)。
假设有一个需求:设计各种类型数据的读写操作。直观的设计是,先设计一个抽象基类,定义数据的读写操作的接口,然后子类继承父类,实现不同数据类型的具体读写操作,代码示例如下:
class Stream
{
public:
// 读文件流
virtual char read(int number) = 0;
// 写文件流
virtual void write(char data) = 0;
virtual ~Stream() {}
};
class FileStream : public Stream
{
public:
virtual char read(int number)
{
cout << "文件类型数据的读操作..." << endl;
}
virtual void write()
{
cout << "文件类型数据的写操作..." << endl;
}
};
class NetworkStream : public Stream
{
public:
virtual char read(int number)
{
cout << "网络类型数据的读操作..." << endl;
}
virtual void write()
{
cout << "网络类型数据的写操作..." << endl;
}
};
class MemoryStream : public Stream
{
public:
virtual char read(int number)
{
cout << "内存类型数据的读操作..." << endl;
}
virtual void write()
{
cout << "内存类型数据的写操作..." << endl;
}
};
现在又增加一个新的需求,要求在读写操作过程中对数据进行加密。为了遵循开放封闭原则,可以创建新的子类,继承实现了读写操作的类,然后重写这些读写操作,实现加密。增加的代码示例如下:
class CryptooFileStream : public FileStream {
public:
virtual char read(int number)
{
// 增加的加密操作
cout << "对文件数据进行加密..." << endl;
// 调用父类的读操作
FileStream::read(number);
}
virtual void write()
{
// 增加的加密操作
cout << "对文件数据进行加密..." << endl;
// 调用父类的写操作
FileStream::write();
}
};
class CryptooNetworkStream : public NetworkStream {
public:
virtual char read(int number)
{
// 增加的加密操作
cout << "对网络数据进行加密..." << endl;
// 调用父类的读操作
NetworkStream::read(number);
}
virtual void write()
{
// 增加的加密操作
cout << "对网络数据进行加密..." << endl;
// 调用父类的写操作
NetworkStream::write();
}
};
class CryptooMemoryStream : public MemoryStream {
public:
virtual char read(int number)
{
// 增加的加密操作
cout << "对内存数据进行加密..." << endl;
// 调用父类的读操作
MemoryStream::read(number);
}
virtual void write()
{
// 增加的加密操作
cout << "对内存数据进行加密..." << endl;
// 调用父类的写操作
MemoryStream::write();
}
};
上述的新需求的实现虽然满足了开放封闭原则,现在看起来没有什么问题,但随着不断的新需求的提出,问题就会迅速暴露出来。例如,现在又提出了一个新的需求,要求在数据读写前,先对其进行缓存操作。可以继续创建新的子类,继承具有读写核心操作功能的类,新增代码示例如下:
class BufferedFileStream : public FileStream {
public:
virtual char read(int number)
{
// 增加的缓存操作
cout << "对内存数据进行缓存..." << endl;
// 调用父类的读操作
FileStream::read(number);
}
virtual void write()
{
cout << "对内存数据进行加密..." << endl;
// 调用父类的写操作
FileStream::write();
}
};
class BufferedNetworkStream : public NetworkStream {
// ...
};
class BufferedMemoryStream : public MemoryStream {
// ...
};
若又有新的需求出现,或者是各种需求的组合,按照上述不断拓展子类的方法会使得子类的数量快速膨胀,变得不易维护。这种通过继承的方式拓展功能的方式如下图所示:
现在做一个假设,新需求的提出不会影响数据的读写操作,且这些新需求,例如加密、缓存等操作不会因数据的类型不同,而操作不同。因此,分析上述通过拓展子类而实现新添加功能的方式,其实是在重复很多相同的操作,如下图对比所示,在新增加的子类中,拓展的新功能的操作是相同的,不同的只是不同类型数据的读写操作。
重复的操作应该把它抽象出来,使其得到复用,进而消除重复,提高代码的可维护性。
可以将上述假设需求的提出总结描述为:在不改变一个类的某个或某些实现的基础上,对这些实现进行拓展;又可说,在不改变类的已有实现的基础上,对这些已有实现新增额外操作。
针对此类情景的问题,可用装饰器模式进行重构。先给出使用装饰器模式对上述代码重构的核心代码:
class CryptoStream : public Stream {
Stream* stream;
public:
CryptoStream(Stream *stm) : stream(stm) {}
virtual char read(int number) {
// 加密操作
stream->read(number);
}
virtual void write() {
// 加密操作
stream->write();
}
};
如上代码所示,只用一个子类就实现了重构前三个子类实现的功能;对重构的代码进行解释:
-
CrytoStream
继承了Stream
,不继承Stream
也没有问题,也能满足需求;继承是为了满足接口规范; -
将具体实现的抽象基类
Stream
对象作为CrytoStream
类的成员,以组合的方式实现拓展功能;为什么将抽象基类作为成员?为了实现多态,将编译时依赖转为运行时依赖。这样就可将“是对那种类型的数据进行加密后再读写”由编译时确定转为运行时再确定。
再进一步的重构,重构核心代码如下:
class DecoratorStream : public Stream {
protected:
Stream *stream;
public:
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 write() {
// 加密操作
stream->write();
}
};
进一步的重构把新增加功能的操作抽象为一个基类,然后实际的新增功能交给子类实现。重构之后类之间的关系就变为如下图所示:
李建忠老师课程中PPT截图
总结
- 装饰器模式中新增加的功能一定要是在不改变原有实现的前提下,这也是设计模式中要遵循的开放封闭原则。
- 通过采用组合而非继承的手法, Decorator模式实现了在运行时 动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免 了使用继承带来的“灵活性差”和“多子类衍生问题”。
- Decorator类在接口上表现为is-a Component的继承关系,即 Decorator类继承了Component类所具有的接口。但在实现上又 表现为has-a Component的组合关系,即Decorator类又使用了 另外一个Component类。
- Decorator模式的目的并非解决“多子类衍生的多继承”问题, Decorator模式应用的要点在于解决“主体类在多个方向上的扩展 功能”——是为“装饰”的含义。