C++ 装饰器模式(Decorator Pattern)
目录
- 模式概述与定义
- 核心解决痛点
- 四大核心角色详解
- 模式结构时序说明
- C++ 标准面向对象实现(咖啡经典案例)
- C++ 函数式轻量装饰器(Lambda/可调用对象)
- 装饰器嵌套组合规则
- 装饰器 VS 适配器 VS 代理模式深度对比
- 适用场景 & 不适用场景
- 优缺点详细拆解
- C++ 工程最佳实践
- 常见坑点与避坑
- 面试高频题及标准答案
- C++ 实际项目落地场景
1. 模式概述与定义
装饰器模式属于结构型设计模式。
定义
动态地给一个对象 添加额外的功能,无需通过继承生成大量子类,也不修改原类源码。
它以包装组合 的方式,层层装饰原有对象,实现功能叠加。
核心设计思想
- 组合替代继承
- 接口保持一致,对外透明
- 动态扩展、任意组合功能
- 遵循开闭原则、单一职责
生活化类比
裸奶茶 → 加珍珠 → 加奶盖 → 加冰;
基础对象不变,配料层层装饰,任意搭配。
2. 核心解决痛点
- 继承泛滥导致类爆炸
每新增一个功能就要新增一个子类,功能组合一多,子类数量指数级增长。 - 功能需要动态按需添加
运行时才决定要不要加日志、缓存、权限、重试。 - 原有业务类禁止修改
核心代码稳定,不能侵入改动,只能外部扩展。 - 功能可自由排列组合
不同业务需要不同功能组合,硬编码无法覆盖。 - 横向扩展能力
统一接口下,新增装饰器无需改动原有代码。
3. 四大核心角色详解
| 角色 | 英文 | 职责说明 |
|---|---|---|
| 抽象组件 | Component | 定义统一接口,原始对象和所有装饰器都实现该接口 |
| 具体组件 | ConcreteComponent | 原始核心对象,实现基础业务功能,被装饰的本体 |
| 抽象装饰器 | Decorator | 继承抽象组件,内部持有抽象组件成员,作为所有装饰器父类 |
| 具体装饰器 | ConcreteDecorator | 在原有功能前后添加额外逻辑,实现功能增强 |
核心约束:装饰器和被装饰对象必须实现同一个接口,客户端完全无感知。
4. 模式调用结构说明
客户端 → 抽象组件接口
→ 具体装饰器(外层)
→ 内部持有下一层组件
→ 最终调用到具体组件真实逻辑
特点:层层包裹、递归调用、接口透明。
5. C++ 标准面向对象实现(经典咖啡案例)
5.1 抽象组件
cpp
#include <iostream>
#include <memory>
#include <string>
// 抽象组件:饮品统一接口
class Beverage
{
public:
virtual ~Beverage() = default;
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
};
5.2 具体组件(基础本体)
cpp
// 具体组件:浓缩咖啡
class Espresso : public Beverage
{
public:
std::string getDescription() const override
{
return "浓缩咖啡";
}
double getCost() const override
{
return 12.0;
}
};
// 具体组件:美式咖啡
class Americano : public Beverage
{
public:
std::string getDescription() const override
{
return "美式咖啡";
}
double getCost() const override
{
return 10.0;
}
};
5.3 抽象装饰器
cpp
// 抽象装饰器
class CondimentDecorator : public Beverage
{
protected:
std::unique_ptr<Beverage> beverage;
public:
explicit CondimentDecorator(std::unique_ptr<Beverage> bev)
: beverage(std::move(bev)) {}
};
5.4 具体装饰器(扩展功能)
cpp
// 牛奶装饰器
class Milk : public CondimentDecorator
{
public:
using CondimentDecorator::CondimentDecorator;
std::string getDescription() const override
{
return beverage->getDescription() + " + 牛奶";
}
double getCost() const override
{
return beverage->getCost() + 3.5;
}
};
// 方糖装饰器
class Sugar : public CondimentDecorator
{
public:
using CondimentDecorator::CondimentDecorator;
std::string getDescription() const override
{
return beverage->getDescription() + " + 方糖";
}
double getCost() const override
{
return beverage->getCost() + 2.0;
}
};
// 焦糖装饰器
class Caramel : public CondimentDecorator
{
public:
using CondimentDecorator::CondimentDecorator;
std::string getDescription() const override
{
return beverage->getDescription() + " + 焦糖";
}
double getCost() const override
{
return beverage->getCost() + 4.0;
}
};
5.5 客户端使用 & 多层嵌套装饰
cpp
int main()
{
// 基础咖啡
auto bev = std::make_unique<Espresso>();
std::cout << bev->getDescription() << " 价格:" << bev->getCost() << "\n";
// 一层装饰:加牛奶
auto withMilk = std::make_unique<Milk>(std::move(bev));
// 二层装饰:再加方糖
auto withSugar = std::make_unique<Sugar>(std::move(withMilk));
// 三层装饰:再加焦糖
auto full = std::make_unique<Caramel>(std::move(withSugar));
std::cout << full->getDescription() << " 价格:" << full->getCost() << "\n";
return 0;
}
6. C++ 轻量函数式装饰器(业务常用)
无需定义类继承,用包装函数实现方法装饰,适合日志、耗时、拦截器。
cpp
#include <functional>
#include <ctime>
// 原始业务函数
void businessFunc(int val)
{
std::cout << "业务执行:" << val << "\n";
}
// 日志装饰器
template<typename Func>
void logDecorator(Func&& f, int val)
{
std::cout << "[日志] 方法开始执行\n";
f(val);
std::cout << "[日志] 方法执行结束\n";
}
// 耗时装饰器
template<typename Func>
void timeDecorator(Func&& f, int val)
{
auto start = clock();
f(val);
auto cost = clock() - start;
std::cout << "[耗时] " << cost << " 时钟周期\n";
}
// 嵌套使用
int main()
{
logDecorator([](int v){
timeDecorator(businessFunc, v);
}, 100);
return 0;
}
7. 装饰器嵌套组合规则
- 接口一致性:所有装饰器必须和原始对象同接口;
- 顺序可调整:装饰先后顺序不同,执行逻辑前后缀不同;
- 无层数限制 :可无限嵌套,工程建议控制在 3层以内;
- 装饰器可复用:同一个装饰器可装饰任意不同具体组件。
8. 装饰器 VS 适配器 VS 代理 深度对比
| 模式 | 类型 | 核心目的 | 接口变化 | 典型场景 |
|---|---|---|---|---|
| 装饰器 | 结构型 | 动态增强功能 | 接口不变 | 日志、缓存、加配料、中间件 |
| 适配器 | 结构型 | 接口转换兼容 | 接口改变 | 旧系统适配、第三方SDK适配 |
| 代理模式 | 结构型 | 控制访问/隔离 | 接口不变 | 远程代理、懒加载、权限控制 |
一句话区分:
- 加功能用装饰器;
- 转接口用适配器;
- 做控制/隔离用代理。
9. 适用场景 & 不适用场景
适用场景
- 需要动态给对象附加功能,且功能可任意组合;
- 继承会造成大量子类爆炸;
- 核心业务类稳定,禁止修改源码;
- 中间件链路、请求拦截、日志、耗时、权限、重试包装;
- 流处理、IO包装、分层过滤系统;
- 商品/饮品/配置类的可选属性叠加。
不适用场景
- 功能固定、无需动态组合;
- 接口差异很大,需要做适配而非增强;
- 简单对象,没必要做多层包装。
10. 优缺点详细拆解
优点
- 遵循开闭原则:新增装饰器不修改原有代码;
- 组合优于继承:彻底解决子类爆炸;
- 动态灵活:运行时按需添加、移除功能;
- 单一职责:每个装饰器只负责一个增强逻辑;
- 透明无感:客户端无需感知是否被装饰。
缺点
- 多层嵌套后调用链路复杂,调试难度上升;
- 会产生大量小粒度装饰器类;
- 装饰顺序会影响最终执行结果,需要业务把控;
- 过度嵌套会带来轻微调用开销。
11. C++ 工程最佳实践
- 统一使用
std::unique_ptr管理组件,避免裸指针内存泄漏; - 装饰器只做前置/后置增强,不改写核心业务逻辑;
- 严格遵守同接口约束,保证透明替换;
- 控制装饰嵌套层数,建议不超过3层;
- 固定常用组合可配合工厂模式封装创建过程;
- 通用横向能力(日志、监控、耗时)优先用函数式装饰器。
12. 常见坑点与避坑
- 坑 :装饰器不继承同一接口,客户端无法统一调用
避坑:强制所有装饰器继承顶层抽象组件。 - 坑 :装饰器内部裸指针管理不当造成内存泄漏
避坑:全程使用智能指针转移所有权。 - 坑 :装饰器中修改原有核心业务逻辑
避坑:装饰只做增强,不侵入本体逻辑。 - 坑 无序堆叠装饰器,业务逻辑混乱
避坑:约定装饰顺序,通用前置拦截放外层。
13. 面试高频题及标准答案
Q1:装饰器模式核心作用?
在不修改原类、不依赖多层继承的前提下,动态给对象叠加额外功能,通过组合包装实现灵活扩展。
Q2:装饰器为什么比继承好?
继承是静态绑定、类爆炸、耦合高;装饰器是动态组合、接口透明、可任意搭配、符合开闭原则。
Q3:装饰器和适配器的区别?
装饰器接口不变,重在功能增强 ;适配器接口转换,重在兼容适配。
Q4:装饰器和代理模式区别?
装饰器侧重功能叠加增强 ;代理侧重访问控制、隔离、懒加载、远程调用。
Q5:C++ 哪些场景适合用装饰器?
日志拦截、耗时统计、权限校验、缓存包装、IO流分层、业务功能动态组合。
14. C++ 实际项目落地场景
- 网络中间件:请求加日志、加签名、加限流、加重试;
- IO流处理:文件流、缓冲流、加密流层层包装;
- 业务服务包装:通用横切逻辑统一装饰;
- 配置/实体类:可选属性动态叠加;
- 游戏角色:装备、Buff 动态加成;
- 接口框架:拦截器、过滤器链路实现。