装饰者模式

648 阅读6分钟

一个例子

星巴兹是一家快速扩张的咖啡店,由于扩张速度太快,星巴兹决定更新订单系统,以满足不同用户的需求。 最开始的设计如下:

Screenshot from 2021-04-09 08-09-46.png

  1. Beverage(饮料)是一个抽象类,店内所提供的饮料都必须继承自此类;
  2. Cost()方法是抽象的,子类必须定义自己的实现;
  3. 每个子类实现Cost()来返回饮料的价格;
  4. 名为description_的实例变量,由每个子类设置,用来描述饮料。利用GetDescription()来返回此描述。

一般来说,咖啡种类有HouseBlend, DarkRoast, Decaf, Espresso。

同时,客户在购买咖啡时,可以购买任意数量的的调料,如蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha)或覆盖奶泡(Whip)。星巴兹会根据所加入的调料收取不同的费用。因此订单系统必须要考虑这些调料。

Screenshot from 2021-04-09 08-12-57.png

很明显,这是一个维护噩梦。如果牛奶价格上涨,怎么办?如果新增一种调料,又怎么办?

一种改进

上述设计的一个问题是每一种组合都创建了一个类,实际上可以用变量来追踪是否添加某种调料。例如: TODO:figure

先从Beverage基类下手,加上实例变量表示是否有某种调料(牛奶,豆浆,摩卡,奶泡等)。

现在Beverage中的Cost()不再是一个抽象方法。我们提供了其具体实现,让它计算要加入的各种调料的调料价格。子类仍要覆盖Cost(),但是会调用父类的Cost(),计算出基本饮料加上调料的价格。

现在加入子类,每个子类代表一种饮料。超类的Cost()会计算所有调料的价钱,而子类覆盖过的Cost()则会扩展超类的功能,把指定的饮料类型的加个也加进来。

每个Cost()方法需要计算该饮料的价格,然后通过调用基类的Cost(),加入调料的价格。

Screenshot from 2021-04-09 08-14-31.png

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,所以不能让你修改现有代码。必须关闭代码以防止被修改。

装饰者模式

现在我们已经意识到利用继承无法完美解决问题,当前遇到的问题有:类爆炸,设计死板,以及基类加入的新功能并非适用于所有子类。

在这里我们采用了不一样的做法:我们以饮料为主体,然后在运行时用调料来装饰饮料。比方说,如果顾客想要摩卡和奶泡深焙咖啡,那么要做的是:

  1. 拿一个深焙咖啡(DarkRoast)对象
  2. 用摩卡(Mocha)对象装饰它;
  3. 用奶泡(Whip)对象装饰它;
  4. 调用Cost(),并且依赖委托将调料的价格加上。

以装饰者构造饮料订单

  1. 以DarkRoast对象开始 Screenshot from 2021-04-09 08-36-56.png

  2. 顾客想要摩卡(Mocha),所以建立一个Mocha对象,并用它将DarkRoast对象包装起来 Screenshot from 2021-04-09 08-38-53.png

  3. 顾客也想要奶泡(Whip),所以需要建立一个Whip装饰者,并用它将Mocha对象包装起来 Screenshot from 2021-04-09 08-37-37.png

  4. 现在,该是为顾客计算钱的时候了。通过调用最外圈装饰者Whip对象的Cost()就可以办得到。Whip的Cost()会先委托它装饰的对象(也就是Mocha)计算出价格,然后再加上奶泡的价格。

    Screenshot from 2021-04-09 08-07-38.png

目前知道的一切

  • 装饰者和被装饰的对象有相同的超类型;
  • 可以用一个或多个装饰者装饰一个对象;
  • 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象被包装的场合,可以用装饰过的对象替代它;
  • 装饰者可以在所委托的被装饰者的行为之前与、或之后,加上自己的行为,以达到特定的目的;
  • 对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用任意装饰者来装饰对象。

定义装饰者模式

装饰者模式动态的将责任附加到对象身上。若要扩展功能,装饰者提供了比继承更加有弹性的替代方案。

类图如下:

image.png

对应到本例中,有如下类图:

Screenshot from 2021-04-09 08-41-01.png

最终代码

菜单:

咖啡价格
HouseBlend0.89
Dark Roast0.99
Espresso1.99
Decat1.05
调料价格
Milk0.10
Soy0.15
Mocha0.20
Whip0.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)(加奶泡);
  • 万一顾客想要双倍的摩卡,怎么办?

装饰器模式的应用场景

前面讲解了关于装饰器模式的结构与特点,下面介绍其适用的应用场景,装饰器模式通常在以下几种情况使用。

  1. 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
  2. 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰器模式却很好实现。
  3. 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

装饰器模式的扩展

装饰器模式所包含的 4 个角色不是任何时候都要存在的,在有些应用环境下模式是可以简化的,如以下两种情况。

  1. 如果只有一个具体构件而没有抽象构件时,可以让抽象装饰继承具体构件。

    image.png

  2. 如果只有一个具体装饰时,可以将抽象装饰和具体装饰合并。

    image.png