装饰器模式详解:动态给对象"穿衣服",C++完整实现
引言
想象一下你在咖啡店点咖啡:你可以点一杯基础的美式咖啡,也可以选择加奶、加糖、加摩卡、加焦糖... 每加一种配料,咖啡的价格和描述都会发生变化。如果用传统的继承方式来实现,你需要为每一种组合都创建一个类:CoffeeWithMilk、CoffeeWithSugar、CoffeeWithMilkAndSugar、CoffeeWithMochaAndMilk... 很快就会出现"类爆炸"问题。
装饰器模式(Decorator Pattern) 正是为了解决这个问题而生的。它是一种结构型设计模式,允许你在运行时动态地给一个对象添加额外的职责,而不需要修改原有对象的代码,也不需要通过继承来扩展功能。
今天我们就用C++语言,从基础概念到完整实现,彻底搞懂装饰器模式。
一、装饰器模式的核心概念
1.1 解决的痛点
在软件开发中,我们经常需要给对象添加新的功能。传统的做法是使用继承:创建一个子类,在子类中添加新的方法或重写父类的方法。但这种方式有几个明显的缺点:
- 类爆炸:每添加一个新功能就需要创建一个新的子类,功能组合越多,类的数量就会呈指数级增长
- 静态继承:继承是静态的,在编译时就确定了,无法在运行时动态改变对象的行为
- 继承层次过深:多层继承会导致代码难以理解和维护
- 违反单一职责原则:一个子类可能包含多个不相关的功能
装饰器模式采用**"组合优于继承"**的设计原则,通过包装对象的方式来动态添加功能,完美解决了这些问题。
1.2 核心思想
装饰器模式的核心思想是:创建一个装饰器类,它包装了原始对象,并且与原始对象实现了相同的接口。这样,客户端可以透明地使用装饰后的对象,就像使用原始对象一样。装饰器可以在调用原始对象的方法前后添加自己的逻辑,从而实现功能的扩展。
你可以把装饰器想象成手机壳:它不会改变手机本身的功能,但可以给手机添加保护、美观、支架等额外功能。你可以给手机套上多个手机壳,每个手机壳都添加不同的功能,而且可以随时取下或更换。
1.3 四个核心角色
装饰器模式包含四个关键角色:
- 抽象组件(Component):定义了对象的通用接口,是具体组件和抽象装饰器共同的父类
- 具体组件(Concrete Component):被装饰的原始对象,实现了抽象组件接口
- 抽象装饰器(Decorator):继承自抽象组件,持有一个抽象组件的引用,用于包装具体组件或其他装饰器
- 具体装饰器(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类型的智能指针。它的作用是统一所有装饰器的接口,使得装饰器可以嵌套使用 - 具体装饰器 :
MilkDecorator、SugarDecorator、MochaDecorator,每个都只负责添加一种配料。它们重写了getDescription()和cost()方法,在原有咖啡的基础上添加自己的描述和价格
最关键的是嵌套装饰的能力:一个装饰器可以包装另一个装饰器,形成一个装饰链。这样我们就可以任意组合不同的配料,而不需要创建新的类。
三、装饰器模式的优缺点
3.1 优点
- 动态添加功能:可以在运行时给对象添加任意数量的功能,比继承灵活得多
- 避免类爆炸:不需要为每一种功能组合创建一个类,大大减少了类的数量
- 符合开闭原则:添加新功能时,只需要创建一个新的具体装饰器类,不需要修改现有代码
- 符合单一职责原则:每个装饰器只负责添加一个功能,职责清晰
- 可以多次装饰:同一个对象可以被多个装饰器多次装饰,实现功能的叠加
- 客户端透明:客户端不需要知道对象是否被装饰过,使用方式完全相同
3.2 缺点
- 产生大量小类:每个具体装饰器都是一个独立的类,会导致系统中出现大量的小类
- 多层装饰比较复杂:如果装饰层数过多,代码的可读性和调试难度会增加
- 容易出现重复装饰:如果不小心,可能会给同一个对象添加多个相同的装饰器
- 无法删除装饰:标准的装饰器模式不支持在运行时删除已经添加的装饰器
四、适用场景
装饰器模式特别适合以下场景:
- 需要动态给对象添加功能,并且这些功能可以动态撤销
- 需要给一个类的多个实例添加不同的功能组合
- 不能使用继承的情况 :
- 类被
final修饰(C++11及以后),无法被继承 - 继承层次太深,导致代码难以维护
- 继承会导致子类数量爆炸
- 类被
- 需要在不影响其他对象的情况下,给单个对象添加功能
- 当采用继承扩展功能不切实际时
经典应用案例:
- Java的IO流体系(
FileInputStream→BufferedInputStream→DataInputStream) - C++ STL中的
std::stack和std::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_);
}
};
七、总结
装饰器模式是一种非常优雅的设计模式,它完美体现了**"组合优于继承"**的设计原则。通过动态包装对象的方式,我们可以在不修改原有代码的情况下,灵活地给对象添加任意数量的功能组合。
在实际开发中,当你遇到以下情况时,应该考虑使用装饰器模式:
- 需要给对象添加多个可以任意组合的功能
- 继承会导致类爆炸或代码难以维护
- 需要在运行时动态改变对象的行为
记住,设计模式不是银弹。装饰器模式虽然强大,但也不能滥用。如果功能组合很少且固定,使用继承可能会更简单。只有当你确实需要动态、灵活地扩展对象功能时,装饰器模式才是最佳选择。