一、概述
继承是对类进行扩展,以提供更多特性的一种基本方法,但是有时候,简单的继承可能不能满足我们的需求。如我们的系统需要提供多种类型的产品:
类型A、类型B、...
同时,这些产品需要支持多种特性:
特性a、特性b、...
以下是两种可能的实现:
1、继承,分别实现类型Aa、类型Ab、类型Ba、类型Bb、...
这种实现方式在类型的数目和所支持特性的数目众多时会造成“类爆炸”,即会引入太多的类型,并且,这种实现的封装性也很差,造成客户代码编写十分困难,十分不可取。
2、修改各类型实现,在其中包含是否支持特性a、特性b、...选项,根据客户选择启用各特性。这种实现是典型的MFC实现方法,但是这种实现只适合特性非常稳定的情况,否则,当特性发生增减时,各类型实现都可能需要修改。
因此,虽然类似的实现屡见不鲜,但以上两种实现方式由于对特性的变化或者类型的变化过于敏感,无法满足类型或特性动态变化的设计需求。如果我们可以将类型和特性分别定义,并且根据客户代码的需要动态对类型和特性进行组合,则可以克服上述问题。
正如Decorator(装饰)模式的名字暗示的那样,Decorator模式可以在我们需要为对象添加一些附加的功能/特性时发挥作用,除此之外,更为关键的是Decorator模式研究的是如何以对客户透明的方式动态地给一个对象附加上更多的特性,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。Decorator模式可以在不创造更多子类的情况下,将对象的功能加以扩展。Decorator模式使用原来被装饰的类的一个子类的实例,把客户端的调用委派到被装饰类,Decorator模式的关键在于这种扩展是完全透明的。
正因为Decorator模式可以动态扩展decoratee所具有的特性,有人将其称为“动态继承模式”,该模式基于继承,但与静态继承下进行功能扩展不同,这种扩展可以被动态赋予decoratee。
二、结构
Decorator模式的结构如下图所示:
图1:Decorator模式类图示意
在上面的类图中包括以下组成部分:
1、Component(抽象构件)角色:给出一个抽象接口,以规范准备接收附加责任的对象。
2、Concrete Component(具体构件)角色:定义一个将要接收附加责任的类。
3、Decorator(装饰)角色:持有一个Component对象的实例,并定义一个与抽象构件接口一致的接口。
4、Concrete Decorator(具体装饰)角色:负责给构件对象“贴上”附加的责任。
三、应用
在以下情况下可以考虑使用Decorator模式:
1、在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。(见示例)
2、处理那些可以撤消的职责。(与上面类似,当有这种动态添加撤销的需求时,可以为类添加相应的装饰类成员,但想要撤销装饰时,将该成员设置为NULL即可,同样,要支持动态切换也很容易)
3、当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈指数增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。(只要你学过排列组合,这一点应该不难理解)
Decorator和Adapter的不同在于前者不改变接口而后者则提供新的接口。可以将Decorator视为一个退化的、仅有一个组件的Composite,然而,Decorator的目的在于给对象添加一些额外的职责,而不是对象聚集。
既然Decorator模式如此强大,是不是可以大加推广,大量运用Decorator来替代简单的继承呢?这样,直接通过继承来扩展类的功能就可以退出历史舞台了!实际上这是不可能的,主要原因如下:
1、虽然“继承破坏了封装性”(父类向子类开放了过多的权限),但是,继承关系是客观世界及OOP中最基本的关系,而且,继承是深化接口规范的基础,没有继承就没有多态等诸多OO特性,因此,继承比Decorator更常见,也更容易定义和实现;
2、由于Decorator动态叠加及不影响decoratee等特性的要求,Decorator很难用于复杂特性的定义;
3、Decorator是一种聚合与继承的结合,应用Decorate模式还存在着其它一些限制,具体将在实现举例部分讨论。
四、优缺点
使用装饰模式主要有以下的优点:
1、装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
2、通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
使用装饰模式主要有以下的缺点:
由于使用装饰模式,可以比使用继承关系需要较少数目的类,使用较少的类,固然使设计比较易于进行,但是,在另一方面,Decorator的缺点是会产生一些极为类似的小型对象,这些小型对象是为了提供极少量的特殊功能而定制的。
五、举例
Decorator模式基本的实现方式如下:Decorator类从待修饰类ConcreteComponent的基类Component派生,以便与ConcreteComponent保持相同的接口,同时,在内部将所有函数调用转发给内部包容的ConcreteComponent对象来执行(1、被包容的ConcreteComponent对象通过Decorator的构造函数传入。2、往往会附加一些“修饰”,否则,Decorator就徒有虚名了)。
在下面的例子中,xsstream用于对sstream进行Decorate,以统计调用operator <<的次数,示例代码如下:
#include <iostream>
using namespace std;
struct stream
{
virtual stream& operator <<(int i) = 0;
virtual stream& operator <<(const char* str) = 0;
};
struct sstream : public stream
{
stream& operator <<(int i)
{
cout << i;
return *this;
}
stream& operator <<(const char* str)
{
cout << str;
return *this;
}
};
class xsstream : public stream
{
static int count;
stream* ps;
public:
xsstream(stream* s) : ps(s) {}
stream& operator <<(int i) {
*ps << "This is [" << ++count << "] call to operator <<. i = " << i << "\n";
return *this;
}
stream& operator <<(const char* str) {
*ps << "This is [" << ++count << "] call to operator <<. str = " << str << "\n";
return *this;
}
};
int xsstream::count = 0;
int main() {
sstream ss;
stream* ps = new xsstream(&ss);
stream& s = *ps;
int i = 1;
s << i << "abc";
delete ps;
return 0;
}
以上方法实现的Decorator模式实质上是对包容的扩展(由于只有一个ConcreteComponent,它看起来很像Proxy模式,但意图不同),虽然以上示例没有什么应用价值,但它基本阐明了Decorator实现的基本方法:
重新定义Decoratee中的接口方法(由于Decorator与Decoratee从相同基类派生,所以这是可能的),在其中添加必要的Decoration,并调用Decoratee的相应方法完成Decoration以外的工作,对于无需修饰的辅助方法,可以直接将方法调用转发给Decoratee。
但是,上述实现同时也告诉了我们Decorator模式存在的一个非常重要的限制,就是:
如果我们从抽象基类派生,我们必须实现抽象基类的每一个虚方法(或者,对于非虚基类,要么重载基类的方法,要么使用基类的实现,但这就丢失了ConcreteComponent子类中重载的实现,即失去(-)了特性,这与Decorator模式进行修饰,即加(+)特性的实质有悖),而当虚方法的数目众多时,这将成为一种负担。如实际basic_ostream实现的operator <<有多种,分别用于bool、short、unsigned short、long、longlong...等等,逐一实现它们是一件很繁琐的事情。
要解决这一问题,可以从sstream而不是stream派生,当我们只需要装饰已经实现的一部分方法时这一招似乎“很管用”,但这有悖于Decorator模式的初衷。因为,Decorator模式提出的目的在于修饰一个类系,而不是单个的类。如果单纯的为了修饰单个的类,简单的继承扩展即可解决问题,根本就无需从Decorator的角度来考虑这个问题,而如果面对的是多个类,这种方式使我们必须再次面对“类组合爆炸”的窘境。
幸好以上这个问题对于基于消息/事件的应用中不存在,如在MFC中,进行界面修饰时,可以只需要特别修饰的消息进行处理,而将所有其它消息转发给待修饰的控件。
这么看来Decorator模式似乎在MFC应用中大有用武之地,但事实并非如此,之所以出现这种局面,大概是因为其一Decorator模式会使得客户代码变得复杂,我们必须自己创建特性,而不是在Create时直接指定,MFC的实现者希望MFC封装类保持与API一样的接口,在将特性在创建控件时静态指定与由用户动态创建并指定之间,MFC的实现者选择了前者;其二,从上面可以看出,Decorator模式对于小特性的定义比较合适,当特性或类系十分复杂时,Decorator模式很难做到面面俱到。
在Java中,java.io.LineNumberReader是一个典型的Decorator,可以Decorate整个Reader类系,用于在读取文件信息的同时统计LineNumber信息,你可以在readLine后通过LineNumberReader.getLineNumber方法获取当前的行号,其实现比较简单,有兴趣的朋友可以研究一下。
参考:
1、Java中Decorate的三种实现方法:http://www.china-dev.com/2004/04/17/10115.html
注:上文讲述的是Decorate,而不是Decorator,如果将Decorate改成Decorator,则部分观点是不恰当的,如JScrollPane修饰JTextArea,严格来讲应用的是组合模式,而不是Decorator模式,阅读时需注意。