装饰者模式
一个例子
星巴兹是一家快速扩张的咖啡店,由于扩张速度太快,星巴兹决定更新订单系统,以满意不同用户的需求。
最开始的规划如下:
- Beverage(饮料)是一个笼统类,店内所供给的饮料都有必要承继自此类;
-
Cost()
办法是笼统的,子类有必要定义自己的结束; - 每个子类结束
Cost()
来回来饮料的价格; - 名为
description_
的实例变量,由每个子类设置,用来描绘饮料。运用GetDescription()
来回来此描绘。
一般来说,咖啡种类有HouseBlend, DarkRoast, Decaf, Espresso。
一起,客户在购买咖啡时,能够购买任意数量的的调料,如蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha)或掩盖奶泡(Whip)。星巴兹会依据所参与的调料收取不同的费用。因而订单系统有必要要考虑这些调料。
很明显,这是一个保护噩梦。假定牛奶价格上涨,怎么办?假定新增一种调料,又怎么办?
一种改善
上述规划的一个问题是每一种组合都创建了一个类,实际上能够用变量来寻觅是否增加某种调料。例如:
TODO:figure
先从Beverage基类下手,加上实例变量表明是否有某种调料(牛奶,豆浆,摩卡,奶泡等)。
现在Beverage中的Cost()
不再是一个笼统办法。咱们供给了其详细结束,让它核算要参与的各种调料的调料价格。子类仍要掩盖Cost()
,可是会调用父类的Cost()
,核算出底子饮料加上调料的价格。
现在参与子类,每个子类代表一种饮料。超类的Cost()
会核算悉数调料的价钱,而子类掩盖过的Cost()
则会扩展超类的功用,把指定的饮料类型的加个也加进来。
每个Cost()
办法需求核算该饮料的价格,然后经过调用基类的Cost()
,参与调料的价格。
Beverage类的结束如下:
class Beverage {
public:
std::string GetDescription() {
if (HasMilk()){
description_ += ", Milk";
}
if (HasSoy()) {
description_ += ", Soy";
}
if (HasWhip()) {
description_ += ", Whip";
}
if (HasMocha()) {
description_ += ", Mocha";
}
return description_;
}
virtual double Cost() {
double condiment_cost = 0;
if (has_milk_){
condiment_cost += milk_cost_;
}
if (has_soy_) {
condiment_cost += soy_cost_;
}
if (has_whip_) {
condiment_cost += whip_cost_;
}
if (has_mocha_) {
condiment_cost += mocha_cost_;
}
return condiment_cost;
}
void SetMilk(bool need){
has_milk_ = need;
}
void SetSoy(bool need){
has_soy_ = need;
}
void SetWhip(bool need){
has_whip_ = need;
}
void SetMocha(bool need){
has_mocha_ = need;
}
private:
double milk_cost_ = 0.10;
double whip_cost_ = 0.10;
double soy_cost_ = 0.15;
double mocha_cost_ = 0.20;
bool has_milk_ = false;
bool has_whip_ = false;
bool has_soy_ = false;
bool has_mocha_ = false;
protected:
std::string description_;
};
详细饮料的结束:
class DarkRoast : public Beverage {
public:
DarkRoast() {
description_ = "Dark Roast";
}
double Cost() {
return Beverage::Cost() + 0.99;
}
};
class Espresso : public Beverage {
public:
Espresso() {
description_ = "Espresso";
}
double Cost() {
return Beverage::Cost() + 1.99;
}
};
制作咖啡:
void offer() {
Beverage* beverage = new Espresso();
std::cout << beverage->GetDescription() << " costs $" << beverage->Cost() << std::endl;
Beverage* beverage2 = new DarkRoast();
beverage2->SetMocha(true);
beverage2->SetWhip(true);
std::cout << beverage2->GetDescription() << " costs $" << beverage2->Cost() << std::endl;
Beverage* beverage3 = new HouseBlend();
beverage3->SetSoy(true);
beverage3->SetMocha(true);
beverage3->SetWhip(true);
std::cout << beverage3->GetDescription() << " costs $" << beverage3->Cost() << std::endl;
}
代码作用:
Espresso costs $1.99
Dark Roast, Whip, Mocha costs $1.29
HouseBlend, Soy, Whip, Mocha costs $1.34
问题:
- 一旦调料价格改变,需求修改基类代码,而且会涉及到悉数子类;
- 一旦呈现新的调料,咱们就需求加上新的办法,而且改动超类中的cost办法;
- 往后或许会开发出新的饮料,关于这些饮料而言(如冰茶),某些调料或许并不适宜,可是在这个规划办法中,茶(Tea)这个子类仍将承继那些不适宜的办法,例如
SetWhip(true)
(加奶泡); - 假定顾客想要双倍的摩卡,怎么办?
翻开-封闭准则
类应该对扩展翻开,对修改封闭
- 翻开:欢迎用任何你想要的行为扩展咱们的类,以满意不同的需求;
- 封闭:咱们花了许多时刻才得到正确的代码,还处理了悉数的bug,所以不能让你修改现有代码。有必要封闭代码以防止被修改。
装修者办法
现在咱们现已意识到运用承继无法完美处理问题,当前遇到的问题有:类爆炸,规划死板,以及基类参与的新功用并非适用于悉数子类。
在这里咱们选用了不一样的做法:咱们以饮料为主体,然后在运行时用调料来装修饮料。比方说,假定顾客想要摩卡和奶泡深焙咖啡,那么要做的是:
- 拿一个深焙咖啡(DarkRoast)政策
- 用摩卡(Mocha)政策装修它;
- 用奶泡(Whip)政策装修它;
- 调用
Cost()
,而且依靠托付将调料的价格加上。
以装修者结构饮料订单
-
现在,该是为顾客核算钱的时分了。经过调用最外圈装修者Whip政策的Cost()就能够办得到。Whip的Cost()会先托付它装修的政策(也便是Mocha)核算出价格,然后再加上奶泡的价格。
现在知道的悉数
- 装修者和被装修的政策有相同的超类型;
- 能够用一个或多个装修者装修一个政策;
- 已然装修者和被装修政策有相同的超类型,所以在任何需求原始政策被包装的场合,能够用装修过的政策代替它;
- 装修者能够在所托付的被装修者的行为之前与、或之后,加上自己的行为,以到达特定的意图;
- 政策能够在任何时分被装修,所以能够在运行时动态地、不限量地用任意装修者来装修政策。
定义装修者办法
装修者办法动态的将责任附加到政策身上。若要扩展功用,装修者供给了比承继更加有弹性的代替方案。
类图如下:
对应到本例中,有如下类图:
终究代码
菜单:
咖啡 | 价格 |
---|---|
HouseBlend | 0.89 |
Dark Roast | 0.99 |
Espresso | 1.99 |
Decat | 1.05 |
调料 | 价格 |
---|---|
Milk | 0.10 |
Soy | 0.15 |
Mocha | 0.20 |
Whip | 0.10 |
饮料基类:
class Beverage {
public:
virtual std::string GetDescription(){
return description_;
}
virtual double Cost() = 0;
protected:
std::string description_ = "Unknown Beverage";
};
class CondimentDecorator : public Beverage {
public:
virtual std::string GetDescription() = 0;
};
写饮料的代码:
class Espresso : public Beverage {
public:
Espresso(){
description_ = "Espresso";
}
double Cost() override {
return 1.99;
};
};
class HouseBlend : public Beverage {
public:
HouseBlend(){
description_ = "House Blend Coffee";
}
double Cost() override {
return 0.89;
};
};
写调料的代码:
class Mocha : public CondimentDecorator {
public:
Mocha(Beverage* beverage){
this.beverage = beverage;
}
std::string GetDescription() override {
return beverage->GetDescription + ", Mocha";
}
double Cost() override {
return beverage->Cost() + 0.20;
}
};
供给咖啡:
void offer(){
Beverage* beverage = new Espresso();
std::cout << beverage->GetDescription() << " costs " << beverage->Cost() << "$";
Beverage* beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
std::cout << beverage2->GetDescription() << " costs " << beverage2->Cost() << "$";
Beverage* beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
std::cout << beverage3->GetDescription() << " costs " << beverage3->Cost() << "$";
}
代码作用:
Espresso costs $1.99
DarkRoast, Mocha, Mocha, Whip costs $1.49
House Blend Coffee, Soy, Mocha, Whip costs $1.34
回忆下之前规划存在的问题:
- 一旦调料价格改变,需求修改基类代码,而且会涉及到悉数子类;
- 一旦呈现新的调料,咱们就需求加上新的办法,而且改动超类中的cost办法;
-
往后或许会开发出新的饮料,关于这些饮料而言(如冰茶),某些调料或许并不适宜,可是在这个规划办法中,茶(Tea)这个子类仍将承继那些不适宜的办法,例如
SetWhip(true)
(加奶泡); - 假定顾客想要双倍的摩卡,怎么办?
装修器办法的运用场景
前面讲解了关于装修器办法的结构与特色,下面介绍其适用的运用场景,装修器办法通常在以下几种状况运用。
- 当需求给一个现有类增加附加责任,而又不能选用生成子类的办法进行扩展时。例如,该类被躲藏或许该类是终极类或许选用承继办法会发生许多的子类。
- 当需求经过对现有的一组底子功用进行排列组合而发生十分多的功用时,选用承继联络很难结束,而选用装修器办法却很好结束。
- 当政策的功用要求能够动态地增加,也能够再动态地撤消时。
装修器办法的扩展
装修器办法所包含的 4 个人物不是任何时分都要存在的,在有些运用环境下办法是能够简化的,如以下两种状况。