使用设计模式可以提高代码的可复用性、可扩充性和可维护性。装饰者模式( Pattern)属于结构型模式,动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。它是通过创建一个包装对象,通过包装对象来包裹真实的对象,以达到装饰目的。
装饰者模式在现实生活中有很多例子,比如一杯咖啡,我们可以往其中加入一些调料,加入巧克力变成摩卡咖啡,加入牛奶变成拿铁咖啡,也可以继续往摩卡或拿铁中加入焦糖、豆浆、奶泡等其他的调料,变成不同风味的咖啡。如果每一种风味的咖啡都变成单独的类,那么不同调料组合而成的咖啡种类数量会非常庞大,会造成类膨胀。每一种调料应该充当装饰者的角色,来修饰咖啡这一具体组件。
装饰模式类结构图:
Component:抽象组件,给具体类对象动态地添加职责。
ConcreteComponent:抽象组件的具体实现类。
Decorator:抽象装饰者,继承Component,用于拓展Component类的功能,但对于Component来说无需知道Decorator的存在。
ConcreteDecorator:装饰者具体实现类。
下面以咖啡为例,以 C++ 为例,使用观察者模式来解决咖啡的计价问题。
咖啡添加了不同的调料,将收取不同的费用。因为调料数量众多,我们不能为每一种口味的咖啡创建一个类。于是我们想到了使用布尔变量进行组合后继承。类设计如下:
超类 Coffee 的数据成员是是否使用对应调料的布尔值,cost() 方法需要计算所有调料的价格,而子类覆盖 cost() 会扩展超类的功能,把指定的调料价格加进去,计算出指定口味咖啡的价格。这样可以大大将少类的个数,但是仔细观察,我们会发现当出现新的调料,不得不修改超类 Coffee。此时,我们需要坚持一个 OO 设计原则:类应该对扩展开放,对修改关闭。
开放关闭原则,允许系统在不修改代码的情况下,进行功能扩展。想想观察者模式,可以在任何时候添加和删除观察者而不需要修改主题代码。本文描述的装饰者模式同样可以做到。
从上面了解到,利用继承无法完全解决问题,咖啡的设计中遇到的问题有:类数量爆炸、设计死板,以及基类加入新功能并不适用于有所的子类。所以,我们的做法是:以原味咖啡为主体,在运行时以调料来"装饰"(decorate)原味咖啡。比方说,如果顾客想要加奶泡(whip)和牛奶(milk)的摩卡,我们的做法是:
(1)拿一个原味咖啡(Coffee)对象;
(2)以巧克力(Chocolate)对象装饰它;
(3)以奶泡(Whip)对象装饰它;
(4)调用cost()方法,并依赖委托(delegate)将调料的价格加上去。
有了上面的步骤,在具体实现上,如何装饰一个对象,而委托又要如何搭配使用呢?请看下面的类图框架:
四个具体组件,每个代表一种咖啡类型。四个具体调料装饰者继承于抽象调料装饰者类(CondimentDecorator),抽象调料装饰者类又继承于抽象原味咖啡类(Coffee)。借助 C++ 具体实现如下:
咖啡基类 Coffee(被装饰者的基类):
class Coffee {
protected:
string description;
public:
virtual double cost() = 0;
virtual string getDescription() {
return description;
}
};
以摩卡咖啡为例,实现具体咖啡类 Mocha,最开始有自己的简单装饰:
class Mocha :public Coffee {
public:
Mocha() {
description="摩卡";
}
//重写,返回价格
virtual double cost() {
return 10;
}
};
抽象调料基类(CondimentDecorator,装饰者基类)用来对原味咖啡进行多层装饰,每层装饰增加一些配料。
class CondimentDecorator :public Coffee {
};
装饰者奶泡类(Whip,装饰者的第一层):
class Whip :public CondimentDecorator {
Coffee* coffee;
public:
Whip(Coffee* coffee) {
this->coffee = coffee;
}
//价格增加0.5元
double cost() {
return 0.5 + coffee->cost();
}
//增加第一层装饰
string getDescription() {
return coffee->getDescription() + "+奶泡";
}
};
装饰者牛奶类(Milk,装饰者的第二层):
class Milk :public CondimentDecorator {
Coffee* coffee;
public:
Milk(Coffee* coffee) {
this->coffee = coffee;
}
//价格增加1.7元
double cost() {
return 1.7 + coffee->cost();
}
//增加第二层装饰
string getDescription() {
return coffee->getDescription() + "+牛奶";
}
};
测试代码:
#include
#include
using namespace std;
int main(){
Coffee* mocha=new Mocha;
cout << mocha->getDescription() << " 价格:" << mocha->cost()<getDescription() << " 价格:" << mocha1->cost() << endl;
//添加牛奶
mocha1 = new Milk(mocha);
cout << mocha1->getDescription() << " 价格:" << mocha1->cost() << endl;
//添加奶泡和牛奶
mocha1 = new Whip(mocha);
mocha1 = new Milk(mocha1);
cout << mocha1->getDescription() << " 价格:" << mocha1->cost() << endl;
system("pause");
}
输出结果:
摩卡 价格:10
摩卡+奶泡 价格:10.5
摩卡+牛奶 价格:11.7
摩卡+奶泡+牛奶 价格:12.2
使用场景:
(1)在不必改变原类和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
(2)当不能采用继承和组合的方式对系统进行扩充或者不利于系统扩展和维护时。
优点:
(1)使用Decorator模式,具体组件类与具体装饰类可以独立变化,用户可以根据需要新增具体组件类和具体装饰类,使用时再对其进行继承,原有代码无须改变,符合“开放关闭原则”。
(2)Decorator模式与继承的目的都是扩展对象功能,有效避免了使用继承的方式扩展对象功能而带来的灵活性差,子类无限制扩张的问题。
(3)通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
缺点:
(1)装饰链不能过长,否则会影响效率。
(2)装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂。
(3)比继承更加灵活机动,同时意味着装饰模式比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐,所以只在必要的时候使用装饰者模式。
(1)OO 设计原则:对扩展开放,对修改关闭,即开放关闭原则。
(2)装饰者模式:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
[1] 装饰者模式.百度百科
[2] 设计模式(七)装饰模式
[3] JAVA设计模式初探之装饰者模式
[4 Freeman E.,Sierra K.,et al.设计模式[M].第一版O’Reilly Taiwan公司译.北京:中国电力出版社,2015:79-105