在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。
Decorator
场景
继PM前天提出了一个需求之后,PM昨天又提出了一个新的需求。
思路
- 没有经验的coder自然倾向于这样的方案
- 原本的代码–>Component类;前天PM提出了需求,加班加点使用ConcreteComponent类( derived from Component 类 ) 进行代码实现;[没毛病]
- 针对昨天PM提出的需求,加班加点使用ConcreteConcreteComponent类( derived from ConcreteComponent 类) 进行实现。[问题来了,没有decorator的思想就很容易掉进的陷阱] ,如下
Stream就是Component,FileStream\NetworkStream\MemoryStream是ConcreteComponent,CryptoFileStream\BufferedFileStream 之类就是ConcreteConcreteComponent
//业务操作
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();
}
-
问题出在哪里?
- “//额外的加密操作…”,对于所有的ConcreteConcreteComponent实例来说都是一样的 --> 重复性的代码出现了 --> bad smell
- 另一方面,如果前天提出的需求可以具体分为m类,昨天的可以分为n类,那么ConcreteConcreteComponent类就会有O(m*n!) 的个数(为什么是阶乘–> 因为需求可以组合,比如我既要加密又要buffer),实现的类会特别多
-
改进如下
//业务操作
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){
}
//...
};
void Process(){
//运行时装配
FileStream* s1=new FileStream();
CryptoStream* s2=new CryptoStream(s1);
BufferedStream* s3=new BufferedStream(s1);
BufferedStream* s4=new BufferedStream(s2);
}
-
把原本继承ConcreteComponent类的ConcreteConcreteComponent类改成直接继承Component,至于前天的需求如何和金今天的需求一起整合呢?–》 使用指针对象
- 利用多态, 所以指针对象是前天的需求的基类,也就是ConcreteComponent的基类–》Component类。
- 所以奇妙的事情发生了,改写后的、实现昨天需求的类,既继承了Component类,又包含了Component类的指针对象
-
一般出现继承就不组合,出现组合就不继承
- 如果看到了对同一个类既有继承又有组合,基本上就是装饰模式
- 这里继承是为了实现接口,组合是为了包含另一个需求/扩展出的功能
-
所以装饰模式就是为了划清责任
- 每个类都负责单一职责 (SRP原则)
-
其实写到现在已经差不多了,但是我们可以进一步优化–> 可以看到改写后的、实现昨天需求的几个类里面都有个特性就是,包含了Steam* stream , 根据原则“看到相同的就可以往上提”(马丁福勒在《重构》中说的),可以在创建一个中间类Decorator继承Stream,然后昨天的需求继承Decorator,这样就隐藏了既继承又组合的现象了。(但实际上工程中上一种写法很常见了。)
uml
- 每次看uml的时候要想清楚哪部分是动的,哪部分是静的,Component和Decorator是静的(稳定点),剩下的Concrete开头的都是动的(不稳定的)。
小总结
- 通过采用组合而非继承的手法, Decorator模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了使用继承带来的“灵活性差”和“多子类衍生问题”。
- Decorator类在接口上表现为is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。但在实现上又表现为has-a Component的组合关系,即Decorator类又使用了另外一个Component类。
- Decorator模式的目的并非解决“多子类衍生的多继承”问题,Decorator模式应用的要点在于解决“主体类在多个方向上的扩展功能”——是为“装饰”的含义。