Menu
CoddyTech

装饰器模式

本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 98 课,共 104 课。

装饰器模式可以让你通过用称为装饰器的特殊对象包装对象,动态地为对象添加新行为。与在编译时添加行为的继承不同,装饰器可以让你在运行时扩展功能,而无需修改原类。

这种模式通过让原始对象和装饰器实现同一个接口来发挥作用。每个装饰器都持有一个组件的引用,并在委托给被包装的对象之前或之后添加自己的行为:

#include <iostream>
#include <memory>

// 组件接口
class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
    virtual ~Coffee() = default;
};

// 具体组件
class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override { return "Coffee"; }
    double getCost() const override { return 2.0; }
};

// 基础装饰器
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

// 具体装饰器
class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Milk";
    }
    double getCost() const override { return coffee->getCost() + 0.5; }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Sugar";
    }
    double getCost() const override { return coffee->getCost() + 0.2; }
};

装饰器可以层层叠加,每个装饰器都会包装前一个,逐层增加功能:

int main() {
    std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
    order = std::make_unique<MilkDecorator>(std::move(order));
    order = std::make_unique<SugarDecorator>(std::move(order));
    
    std::cout << order->getDescription() << ": $" << order->getCost();
    // 输出:Coffee, Milk, Sugar: $2.7
}

当你需要动态地为对象添加职责,而不影响其他对象,或者通过创建子类来扩展功能会导致类的数量激增时,可以使用 Decorator。

challenge icon

挑战

简单

让我们使用装饰器模式构建一个披萨点单系统。你将创建一个灵活的系统,让顾客可以从基础披萨开始,再添加各种配料:每种配料都会包装上一个订单,并增加自己的价格和描述。这正是装饰器的理想应用场景:配料可以任意组合叠加,而不必创建大量子类。

你将把代码分布在三个文件中:

  • Pizza.h:定义组件接口和基础披萨类。

    创建一个抽象的 Pizza 类,其中包含纯虚方法 getDescription()(返回字符串)和 getCost()(返回 double),以及一个虚析构函数。

    然后创建一个具体的 PlainPizza 类,表示基础披萨。它的描述应返回 "Pizza",基础价格为 8.0。

  • ToppingDecorator.h:构建装饰器基类和具体的配料装饰器。

    创建一个继承自 Pizza 的 ToppingDecorator 类,并让它持有一个指向被包装组件的 std::unique_ptr<Pizza>。它将作为所有配料装饰器的基类。

    实现三个具体的配料装饰器:

    • CheeseTopping:在描述中添加 ", Cheese",并在价格中添加 1.5
    • PepperoniTopping:在描述中添加 ", Pepperoni",并在价格中添加 2.0
    • MushroomTopping:在描述中添加 ", Mushrooms",并在价格中添加 1.0

    每个装饰器都应先委托给被包装的披萨,然后添加自己的部分。

  • main.cpp:根据用户输入创建定制披萨订单。

    读取三个输入(每个输入都是 yes 或 no):

    1. 添加奶酪?
    2. 添加意大利辣香肠?
    3. 添加蘑菇?

    从一个 PlainPizza 开始,然后根据输入使用相应的配料装饰器依次包装它(顺序为:奶酪、意大利辣香肠、蘑菇)。最后,按以下格式打印订单详情:

    Order: [description]
    Total: $[cost]

例如,输入 yes、yes、no 时:

Order: Pizza, Cheese, Pepperoni
Total: $11.5

输入 yes、no、yes 时:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

输入 no、no、no 时:

Order: Pizza
Total: $8

输入 yes、yes、yes 时:

Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5

注意,每个装饰器都会包装上一个装饰器,逐层累加描述和价格。你可以通过创建新的装饰器类轻松添加配料,而无需修改现有的披萨或配料代码,这就是装饰器模式的妙处。

亲手试试

#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"

using namespace std;

int main() {
    // 读取 3 个输入(yes 或 no)
    string addCheese, addPepperoni, addMushrooms;
    cin >> addCheese;
    cin >> addPepperoni;
    cin >> addMushrooms;
    
    // TODO: 使用 std::unique_ptr,从 PlainPizza 开始
    
    // TODO: 如果 addCheese 是 "yes",就用 CheeseTopping 包装披萨
    
    // TODO: 如果 addPepperoni 是 "yes",就用 PepperoniTopping 包装披萨
    
    // TODO: 如果 addMushrooms 是 "yes",就用 MushroomTopping 包装披萨
    
    // TODO: 按以下格式输出订单:
    // 订单:[description]
    // 总计:$[cost]
    
    return 0;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器