装饰,即为扩展。为一个主体扩展一些其他操作。而装饰中间类即为扩展提供了一个规范,如此变可以通过组合的方式,完整复杂的多功能扩展。
代码:流操作
第一个版本
流操作的需求
- 流的类型:文件流、网络流、内存流
- 流的操作:加密、缓存
对于这些流操作的需求,我们很容易想到要有一个基类。
//业务操作
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();
}
继承关系:
问题一:类的规模
- Stream这一层是1,假设第二层的变化是n个,第三层的变化是m
- 不是m*n,他们可能有组合的情况,既加密又缓存
- 第三层结点个数:
n*(m+ m-1 + m-2 + m-3 ... + 1)
问题二:代码冗余
- 加密操作其实都是一样的(
CryptoMemoryStream::Read
、CryptoNetworkStream::Read
、CryptoFileStream::Read
三者的加密操作其实是一样的) - 缓存的操作其实都是一样的
优化:去除代码冗余
根据组合优于继承原则,我们将继承改为组合
//...
//第一种扩展操作:加密
class CryptoFileStream /*:public FileStream*/
//删除继承关系
{
FileStream* stream; //改为组合关系
public:
virtual char Read(int number){
//额外的加密操作...
//FileStream::Read(number); //调用父类读文件流的方法
//改为直接用字段的函数
stream->Read(number);
}
//...
};
class CryptoNetworkStream /*:public NetworkStream*/
{
NetworkStream* stream;
public:
virtual char Read(int number){
//额外的加密操作...
//NetworkStream::Read(number);//读网络流
stream->Read(number);
}
//...
};
class CryptoMemoryStream /*: public MemoryStream*/
{
MemoryStream* stream;
public:
virtual char Read(int number){
//额外的加密操作...
//MemoryStream::Read(number);//读内存流
stream->Read(number);
}
//...
};
优化:使用父类多态
当一个变量的声明类型有共同的父类时,我们可以直接声明为它们的父类——马丁·福勒经典重构理论
在上个版本中,CryptoFileStream::stream
、CryptoNetworkStream::stream
、CryptoMemoryStream::stream
三个变量的声明类型有共同的父类,即Stream
。
我们可以直接把它们写成Stream
类型的指针,通过多态来调用函数
在未来,使用的时候,再传具体的类型(在运行时指定具体类型,编译时不指定)
//...
//第一种扩展操作:加密
class CryptoFileStream
{
Stream* stream; //在未来,赋值为FileStream。即= new FileStream();
public:
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);
}
//...
};
class CryptoNetworkStream
{
Stream* stream; // = new NetworkStream();
public:
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);
}
//...
};
class CryptoMemoryStream
{
Stream* stream; // = new MemoryStream();
public:
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);
}
//...
};
优化:抽出父类(第二个大版本)
经过上两个版本的迭代,发现CryptoFileStream
、CryptoNetworkStream
、CryptoMemoryStream
变成一样了。只不过字段stream的具体类型需要在运行时指定。
class CryptoStream
: public Stream //继承Stream基类,为了让Read、Seek、Write函数满足接口规范(和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 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){
}
//...
};
//运行时装配
//1. 编译不存在加密文件流、缓存文件流、加密缓存文件流。我们没有那样的类
//2. 但是在运行的时候,我们可以通过组合的方式把它们装配起来,满足我们的需求
void Process(){
//创建一个文件流
FileStream* s1 = new FileStream();
//传入s1文件流,s2为文件流的加密操作
CryptoStream* s2 = new CryptoStream(s1);
//传入s1文件流,s3为文件流的缓存操作
BufferedStream* s3 = new BufferedStream(s1);
//传入s2文件流加密操作,s4为文件流加密操作 + 缓存操作
BufferedStream* s4 = new BufferedStream(s2);
}
优化:再抽象出父类,即装饰器(第三个大版本)
其实不难看出,CryptoStream
、BufferedStream
还存在冗余代码。有着一个相同的成员变量。
如果某一个类的多个子类有着共同的字段,我们可以把这个字段往上提一层——马丁·福勒经典重构理论
Stream
的子类CryptoStream
、BufferedStream
有着共同的字段Stream* stream
,所以我们可以把它们往上再提一层
我们有两种提法。
一、第一种提法:把Stream* stream
提到基类中(即Stream
里)
class Stream{
Stream* stream;
public:
virtual char Read(int number)=0;
virtual void Seek(int position)=0;
virtual void Write(char data)=0;
virtual ~Stream(){}
};
提到父类Stream中,但这会有一个问题。主体类FileStream
、NetworkStream
、MemoryStream
都不需要这个字段
二、第二种提法:设计一个中间类
//中间的装饰类
class DecoratorStream: public Stream{
protected: //子类要用,申明为protected
Stream* stream;//...
DecoratorStream(Stream * stm):stream(stm){
}
};
第二种提法是可以的,其实就是我们的装饰设计模式
完整代码:
//业务操作
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){
//写内存流
}
};
//提供一个中间装饰类,为Stream的扩展操作提供一个基类
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{
//与CryptoStream类似
};
void Process(){
//运行时装配
FileStream* s1=new FileStream(); //只需要一个文件流,没有加密、缓存的操作
//加密流必须传入一个流进来,这就是扩展操作,你在谁的基础上再做操作
//这就是装饰的含义,附着在其他之上的操作
CryptoStream* s2=new CryptoStream(s1); //文件加密流
BufferedStream* s3=new BufferedStream(s1); //文件缓存流
BufferedStream* s4=new BufferedStream(s2);
}
说明:装饰的含义,就是附着在本体之上的其他操作,即扩展操作。所以如果你要使用装饰的话,必须指定其本体是谁
例如:
FileStream* s1=new FileStream();
//只需要一个文件流,没有加密、缓存的操作
//加密流(加密流是装饰类的子类),它必须传入一个流进来
//这就是扩展操作,你在谁的基础上再做操作
//这就是装饰的含义,附着在本体之上的操作
CryptoStream* s2=new CryptoStream(s1); //文件加密流
第一个版本 | 第三个版本(装饰模式) | |
---|---|---|
类图 | ||
类规模 | 1+n+n*m!/2 |
**1+n+1+m** |
为什么两个版本类规模差异如此之大?
- 第一个版本背后有很多重复代码
- 是对继承的不良使用,使用组合更好。用组合的方式支持未来的一种变化
**
举例:每个英雄都可以放QWER技能
动机
在某些情况下我们可能会“过渡地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
- 静态特质:第一个版本
CryptoFileStream
类中FileStream::Read(number);
,这永远是调用FileStream的Read函数,是不会变化的代码。这个是由继承实现的 - 动态特质:在第三个版本中,
CryptoStream
类中的stream->Read(number)
。这句代码是用多态指针来调虚函数,这句话就有变化了。这个是由组合实现的
如何使“对象功能的扩展”能够根据需要来动态地实现?
同时避免“扩展功能的增多”带来子类膨胀问题?
从而使得任何“功能扩展变化”所导致的影响降为最低?
模式定义
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)。——《设计模式》GOF
结构
要点总结
要点一: 通过采用组合而非继承的手法,Decorator模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了使用继承带来的“灵活性差”和“多子类衍生问题”
要点二:Decorator类在接口上表现为is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。但在实现上又表现为has-a Component的组合关系,即Decorator类又使用了另外一个Component类
- Decorator类既继承Stream,又组合Stream(内部有一个Stream类型的多态指针)
- 如果一个类B继承与A类,又具有一个A类的指针 => 99%是Decorator设计模式 => 正常情况不会这么干,要不就只有继承,要不就只有组合,不会两个都有;两个都有的情况一般都是装饰模式
- 组合Steam是为了什么?将来支持调用Stream的具体实现类(如FileStream、NetworkStream、MemoryStream)
- 继承Stream是为了什么?把它自己也声明为一个流,为了让Read、Seek、Write函数满足接口规范(和stream的函数一样),成为一个虚函数
- 有时候你可能看不到人家的内部字段(看不到人家有组合)。但类B继承于A,B的构造函数里还需要传A的指针,你也能判断B内部有A,B和A也有组合关系
要点三:Decorator模式的目的并非解决“多子类衍生的多继承”问题,Decorator模式应用的要点在于解决“主体类在多个方向上的扩展功能”——是为了“装饰”的含义
- FileStream和Stream是继承关系,但是CryptoFileStream和FileStream是继承关系嘛?非要继承吗?
- 不是的,它俩(Stream->FileStream;FileStream->CryptoFileStream)都不是一个变化的方向
- 实际上FileStream->CryptoFileStream是扩展操作,是扩展的方向;而Stream->FileStream是主体操作
- 主体操作和扩展操作应该分开成两个分支进行继承