设计模式入门:3. 装饰器模式详解 C++实现

装饰器模式详解:动态给对象"穿衣服",C++完整实现

引言

想象一下你在咖啡店点咖啡:你可以点一杯基础的美式咖啡,也可以选择加奶、加糖、加摩卡、加焦糖... 每加一种配料,咖啡的价格和描述都会发生变化。如果用传统的继承方式来实现,你需要为每一种组合都创建一个类:CoffeeWithMilkCoffeeWithSugarCoffeeWithMilkAndSugarCoffeeWithMochaAndMilk... 很快就会出现"类爆炸"问题。

装饰器模式(Decorator Pattern) 正是为了解决这个问题而生的。它是一种结构型设计模式,允许你在运行时动态地给一个对象添加额外的职责,而不需要修改原有对象的代码,也不需要通过继承来扩展功能。

今天我们就用C++语言,从基础概念到完整实现,彻底搞懂装饰器模式。


一、装饰器模式的核心概念

1.1 解决的痛点

在软件开发中,我们经常需要给对象添加新的功能。传统的做法是使用继承:创建一个子类,在子类中添加新的方法或重写父类的方法。但这种方式有几个明显的缺点:

  1. 类爆炸:每添加一个新功能就需要创建一个新的子类,功能组合越多,类的数量就会呈指数级增长
  2. 静态继承:继承是静态的,在编译时就确定了,无法在运行时动态改变对象的行为
  3. 继承层次过深:多层继承会导致代码难以理解和维护
  4. 违反单一职责原则:一个子类可能包含多个不相关的功能

装饰器模式采用**"组合优于继承"**的设计原则,通过包装对象的方式来动态添加功能,完美解决了这些问题。

1.2 核心思想

装饰器模式的核心思想是:创建一个装饰器类,它包装了原始对象,并且与原始对象实现了相同的接口。这样,客户端可以透明地使用装饰后的对象,就像使用原始对象一样。装饰器可以在调用原始对象的方法前后添加自己的逻辑,从而实现功能的扩展。

你可以把装饰器想象成手机壳:它不会改变手机本身的功能,但可以给手机添加保护、美观、支架等额外功能。你可以给手机套上多个手机壳,每个手机壳都添加不同的功能,而且可以随时取下或更换。

1.3 四个核心角色

装饰器模式包含四个关键角色:

  1. 抽象组件(Component):定义了对象的通用接口,是具体组件和抽象装饰器共同的父类
  2. 具体组件(Concrete Component):被装饰的原始对象,实现了抽象组件接口
  3. 抽象装饰器(Decorator):继承自抽象组件,持有一个抽象组件的引用,用于包装具体组件或其他装饰器
  4. 具体装饰器(Concrete Decorator):实现了具体的扩展功能,在调用原始对象方法的前后添加自己的逻辑

二、标准装饰器模式实现

2.1 UML类图

复制代码
             +----------------+
             |   Component    | <-- 抽象组件
             +----------------+
             | + operation()  |
             +----------------+
                  ^ ^
                 /   \
                /     \
+----------------+   +----------------+
| ConcreteComp   |   |   Decorator    | <-- 抽象装饰器
+----------------+   +----------------+
| + operation()  |   | - component:   |
+----------------+   |   Component*   |
                     +----------------+
                            ^
                            |
                   +----------------+
                   | ConcreteDecor  | <-- 具体装饰器
                   +----------------+
                   | + operation()  |
                   +----------------+

2.2 C++实现(咖啡例子)

我们就用开头提到的咖啡例子来实现装饰器模式。我们有基础的简单咖啡,可以动态添加奶、糖、摩卡等配料。

cpp 复制代码
#include <iostream>
#include <string>
#include <memory> // 现代C++智能指针

// 抽象组件:咖啡
class Coffee {
public:
    virtual ~Coffee() = default;
    virtual std::string getDescription() const = 0; // 获取咖啡描述
    virtual double cost() const = 0; // 获取咖啡价格
};

// 具体组件:简单咖啡(被装饰的原始对象)
class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override {
        return "简单咖啡";
    }

    double cost() const override {
        return 10.0; // 基础价格10元
    }
};

// 抽象装饰器:咖啡装饰器
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee_; // 持有被装饰的咖啡对象

public:
    // 构造函数接收一个咖啡对象
    explicit CoffeeDecorator(std::unique_ptr<Coffee> coffee) 
        : coffee_(std::move(coffee)) {}
};

// 具体装饰器:加奶
class MilkDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator; // 继承构造函数

    std::string getDescription() const override {
        return coffee_->getDescription() + " + 牛奶";
    }

    double cost() const override {
        return coffee_->cost() + 2.0; // 加奶加2元
    }
};

// 具体装饰器:加糖
class SugarDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;

    std::string getDescription() const override {
        return coffee_->getDescription() + " + 糖";
    }

    double cost() const override {
        return coffee_->cost() + 1.0; // 加糖加1元
    }
};

// 具体装饰器:加摩卡
class MochaDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;

    std::string getDescription() const override {
        return coffee_->getDescription() + " + 摩卡";
    }

    double cost() const override {
        return coffee_->cost() + 5.0; // 加摩卡加5元
    }
};

// 客户端代码
int main() {
    // 1. 简单咖啡
    std::unique_ptr<Coffee> coffee1 = std::make_unique<SimpleCoffee>();
    std::cout << "咖啡1: " << coffee1->getDescription() 
              << ",价格: " << coffee1->cost() << "元" << std::endl;

    // 2. 加奶咖啡
    std::unique_ptr<Coffee> coffee2 = std::make_unique<MilkDecorator>(
        std::make_unique<SimpleCoffee>()
    );
    std::cout << "咖啡2: " << coffee2->getDescription() 
              << ",价格: " << coffee2->cost() << "元" << std::endl;

    // 3. 加奶加糖咖啡(嵌套装饰)
    std::unique_ptr<Coffee> coffee3 = std::make_unique<SugarDecorator>(
        std::make_unique<MilkDecorator>(
            std::make_unique<SimpleCoffee>()
        )
    );
    std::cout << "咖啡3: " << coffee3->getDescription() 
              << ",价格: " << coffee3->cost() << "元" << std::endl;

    // 4. 豪华咖啡:摩卡+奶+糖(任意组合)
    std::unique_ptr<Coffee> coffee4 = std::make_unique<MochaDecorator>(
        std::make_unique<MilkDecorator>(
            std::make_unique<SugarDecorator>(
                std::make_unique<SimpleCoffee>()
            )
        )
    );
    std::cout << "咖啡4: " << coffee4->getDescription() 
              << ",价格: " << coffee4->cost() << "元" << std::endl;

    return 0;
}

2.3 运行结果

复制代码
咖啡1: 简单咖啡,价格: 10元
咖啡2: 简单咖啡 + 牛奶,价格: 12元
咖啡3: 简单咖啡 + 牛奶 + 糖,价格: 13元
咖啡4: 简单咖啡 + 糖 + 牛奶 + 摩卡,价格: 18元

2.4 代码解析

  • 抽象组件Coffee :定义了所有咖啡都必须实现的两个方法:getDescription()cost()
  • 具体组件SimpleCoffee:最基础的咖啡,实现了抽象组件的接口
  • 抽象装饰器CoffeeDecorator :继承自Coffee,并且持有一个Coffee类型的智能指针。它的作用是统一所有装饰器的接口,使得装饰器可以嵌套使用
  • 具体装饰器MilkDecoratorSugarDecoratorMochaDecorator,每个都只负责添加一种配料。它们重写了getDescription()cost()方法,在原有咖啡的基础上添加自己的描述和价格

最关键的是嵌套装饰的能力:一个装饰器可以包装另一个装饰器,形成一个装饰链。这样我们就可以任意组合不同的配料,而不需要创建新的类。


三、装饰器模式的优缺点

3.1 优点

  1. 动态添加功能:可以在运行时给对象添加任意数量的功能,比继承灵活得多
  2. 避免类爆炸:不需要为每一种功能组合创建一个类,大大减少了类的数量
  3. 符合开闭原则:添加新功能时,只需要创建一个新的具体装饰器类,不需要修改现有代码
  4. 符合单一职责原则:每个装饰器只负责添加一个功能,职责清晰
  5. 可以多次装饰:同一个对象可以被多个装饰器多次装饰,实现功能的叠加
  6. 客户端透明:客户端不需要知道对象是否被装饰过,使用方式完全相同

3.2 缺点

  1. 产生大量小类:每个具体装饰器都是一个独立的类,会导致系统中出现大量的小类
  2. 多层装饰比较复杂:如果装饰层数过多,代码的可读性和调试难度会增加
  3. 容易出现重复装饰:如果不小心,可能会给同一个对象添加多个相同的装饰器
  4. 无法删除装饰:标准的装饰器模式不支持在运行时删除已经添加的装饰器

四、适用场景

装饰器模式特别适合以下场景:

  1. 需要动态给对象添加功能,并且这些功能可以动态撤销
  2. 需要给一个类的多个实例添加不同的功能组合
  3. 不能使用继承的情况
    • 类被final修饰(C++11及以后),无法被继承
    • 继承层次太深,导致代码难以维护
    • 继承会导致子类数量爆炸
  4. 需要在不影响其他对象的情况下,给单个对象添加功能
  5. 当采用继承扩展功能不切实际时

经典应用案例

  • Java的IO流体系(FileInputStreamBufferedInputStreamDataInputStream
  • C++ STL中的std::stackstd::queue(本质上是对std::deque的装饰)
  • GUI组件的装饰(给按钮添加边框、阴影、动画等)
  • 日志系统的装饰(给日志添加时间戳、线程ID、级别等信息)

五、与其他模式的对比

很多人容易把装饰器模式和其他结构型模式混淆,这里做一个清晰的对比:

模式 核心目的 与装饰器的区别
装饰器模式 动态给对象添加额外功能 不改变接口,增强功能,支持嵌套
适配器模式 转换接口,让不兼容的类一起工作 改变接口,不改变功能
代理模式 控制对对象的访问 不改变接口,控制访问,通常只包装一层
桥接模式 将抽象与实现分离,使它们可以独立变化 分离两个独立变化的维度,而不是动态添加功能
组合模式 将对象组合成树形结构以表示"部分-整体"层次 处理对象的组合关系,而不是添加功能

六、现代C++改进与变种

6.1 使用模板简化装饰器

如果我们有多个不同的抽象组件,每个都需要写一个抽象装饰器类,会比较繁琐。使用C++模板可以简化这个过程:

cpp 复制代码
// 通用模板装饰器
template <typename Component>
class TemplateDecorator : public Component {
protected:
    std::unique_ptr<Component> component_;

public:
    explicit TemplateDecorator(std::unique_ptr<Component> component)
        : component_(std::move(component)) {}
};

// 使用模板装饰器定义具体装饰器
class MilkDecorator : public TemplateDecorator<Coffee> {
public:
    using TemplateDecorator::TemplateDecorator;

    std::string getDescription() const override {
        return component_->getDescription() + " + 牛奶";
    }

    double cost() const override {
        return component_->cost() + 2.0;
    }
};

6.2 函数式装饰器(C++11及以后)

对于只有一个方法的接口,我们可以使用std::function和Lambda表达式来实现更简洁的函数式装饰器:

cpp 复制代码
#include <functional>

// 定义咖啡函数类型
using CoffeeFunction = std::function<std::pair<std::string, double>()>;

// 基础咖啡函数
CoffeeFunction simpleCoffee() {
    return []() {
        return std::make_pair("简单咖啡", 10.0);
    };
}

// 加奶装饰器函数
CoffeeFunction withMilk(CoffeeFunction coffee) {
    return [coffee]() {
        auto [desc, cost] = coffee();
        return std::make_pair(desc + " + 牛奶", cost + 2.0);
    };
}

// 加糖装饰器函数
CoffeeFunction withSugar(CoffeeFunction coffee) {
    return [coffee]() {
        auto [desc, cost] = coffee();
        return std::make_pair(desc + " + 糖", cost + 1.0);
    };
}

// 客户端代码
int main() {
    auto coffee = withMilk(withSugar(simpleCoffee()));
    auto [desc, cost] = coffee();
    std::cout << "函数式装饰器: " << desc << ",价格: " << cost << "元" << std::endl;
    return 0;
}

这种方式非常简洁,不需要定义任何类,适合简单的装饰场景。

6.3 可移除装饰器

标准的装饰器模式不支持在运行时移除装饰器。如果需要这个功能,可以在抽象装饰器中添加一个getComponent()方法,让客户端可以访问被包装的对象:

cpp 复制代码
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee_;

public:
    explicit CoffeeDecorator(std::unique_ptr<Coffee> coffee)
        : coffee_(std::move(coffee)) {}

    // 获取被包装的对象
    std::unique_ptr<Coffee> getComponent() {
        return std::move(coffee_);
    }
};

七、总结

装饰器模式是一种非常优雅的设计模式,它完美体现了**"组合优于继承"**的设计原则。通过动态包装对象的方式,我们可以在不修改原有代码的情况下,灵活地给对象添加任意数量的功能组合。

在实际开发中,当你遇到以下情况时,应该考虑使用装饰器模式:

  • 需要给对象添加多个可以任意组合的功能
  • 继承会导致类爆炸或代码难以维护
  • 需要在运行时动态改变对象的行为

记住,设计模式不是银弹。装饰器模式虽然强大,但也不能滥用。如果功能组合很少且固定,使用继承可能会更简单。只有当你确实需要动态、灵活地扩展对象功能时,装饰器模式才是最佳选择。

相关推荐
程序大视界1 小时前
【C++ 从基础到项目实战】C++(三):函数进阶——重载、回调、递归与默认参数
开发语言·c++·cpp
西梅汁1 小时前
C++ 线程间通信(二)
c++
咖啡八杯1 小时前
GoF设计模式——装饰模式
java·算法·设计模式·装饰器模式
minji...1 小时前
Linux 高级IO(七)多进程、多线程的Reactor反应堆模式扩展、OTOL
linux·运维·c++·多路转接·epoll·reactor反应堆模型
晚风吹红霞1 小时前
C++ list 容器完全指南:从入门到手撕双向链表
c++·链表·list
handler011 小时前
【Linux 网络】:poll/epoll 底层机制与 Reactor 并发模型
linux·运维·服务器·网络·c++·多路转接·多路复用
cpp_25011 小时前
P10109 [GESP202312 六级] 工作沟通
数据结构·c++·算法·题解·洛谷·gesp六级
Xeon_CC1 小时前
vs2026远程开发debian12容器的C++程序笔记
开发语言·c++·笔记
玉树临风ives1 小时前
atcoder ABC 460 题解
数据结构·c++·算法