🎯 导读:装饰模式是面向对象设计中极其精妙的结构型模式。如果说”策略模式”教你切换核心算法,那么”装饰模式”教你在不改变原有代码、也不使用无脑继承的情况下,动态地给对象叠加新功能。本文配有 📊 UML类图 和 💻 完整代码案例,带你彻底掌握这个”套娃”技巧!
🎭 1. 生动的比喻:楼下的煎饼果子摊
理解装饰模式最经典的现实场景,就是买煎饼果子(或者买奶茶)。
❌ 没有装饰模式(滥用继承的噩梦)
假设煎饼摊的老板是个没经验的程序员。他卖基础的”原味煎饼”。
| 顾客需求 | 老板的类设计 |
|---|---|
| 加个蛋 | class 鸡蛋煎饼 : public 原味煎饼 |
| 加根火腿 | class 火腿煎饼 : public 原味煎饼 |
| 加蛋加火腿 | class 鸡蛋火腿煎饼 : public 原味煎饼 |
| 再加生菜、辣条、薄脆… | 🤯 类爆炸💥!几十上百个子类! |
🔥 问题:这就是典型的**”类爆炸”**,维护成本极高!
✅ 使用装饰模式(动态套娃)
聪明的摊主(架构师)转变了思路:
1 | 🥞 原味煎饼 (核心) |
✅ 优势:
- 🧩 只提供一个基础的
原味煎饼对象 - 🎁 所有配料都是装饰器
- 🔄 运行时动态组合,没有任何多余的子类!
📊 2. UML 类图与核心角色
装饰模式的魔法在于:装饰器不仅”拥有”组件(组合关系),它本身也”是”一个组件(泛化关系)。
classDiagram
class IPancake {
+ getDescription()
+ cost()
}
class BasePancake {
+ getDescription()
+ cost()
}
class CondimentDecorator {
# wrappedPancake
+ getDescription()
+ cost()
}
class EggDecorator {
+ getDescription()
+ cost()
}
class SausageDecorator {
+ getDescription()
+ cost()
}
IPancake <|.. BasePancake : 实现
IPancake <|.. CondimentDecorator : 实现
CondimentDecorator <|-- EggDecorator : 继承
CondimentDecorator <|-- SausageDecorator : 继承
CondimentDecorator o-- IPancake : 组合
🔑 四大核心角色
| 角色 | 类名 | 职责 | 比喻 |
|---|---|---|---|
| 🎯 抽象构件 | IPancake |
定义基础契约(cost(), desc()) |
煎饼的标准规格 |
| 🥞 具体构件 | BasePancake |
核心基础对象 | 原味煎饼 |
| 🎁 抽象装饰类 | CondimentDecorator |
魔法所在! 继承+组合双重身份 | 配料包装盒 |
| 🥚🌭 具体装饰类 | EggDecorator, SausageDecorator |
具体配料,附加功能 | 鸡蛋、火腿 |
💡 魔法解析:
CondimentDecorator既继承了IPancake(是一个煎饼),又组合了IPancake(内部包着一个煎饼)。这使得它可以把自己伪装成普通组件,去替换被它包装的组件!
💻 3. C++ 现代代码生动演绎
🎯 智能指针版(推荐)
1 |
|
📤 输出结果
1 | === 顾客 A:老板,来个原味煎饼。 === |
🔧 普通指针版(兼容旧代码)
1 |
|
🔍 深入理解:指针传递与内存释放全过程
别担心,装饰模式的指针传递和内存释放过程,确实是很多初学者最容易绕晕的地方。把它想象成 俄罗斯套娃 或者 包快递 的过程,就会非常清晰了。我们一行一行来拆解,看看在计算机内存里到底发生了什么。
第一阶段:构造过程(从内向外包)
在代码中,我们是这样写的:
1 | // 第 1 步 |
我们来看看内存里是怎么变化的:
第 1 步:摊基础煎饼
- 内存中生成了一个
BasePancake对象(我们叫它 对象A)。 - 指针
luxuryBreakfast目前指向 对象A。
1 | 内存状态: |
第 2 步:加鸡蛋
new EggDecorator(...)在内存中生成了一个鸡蛋包装盒(对象B)。- 构造时,我们把当前的
luxuryBreakfast(也就是 对象A 的地址)传给了它。所以,对象B 的肚子里(wrappedPancake指针)装进了 对象A。 - 然后,代码最前面的
=号,让luxuryBreakfast指针改变了指向,它现在指向了最外层的 对象B。
1 | 内存状态: |
第 3 步:加火腿
new SausageDecorator(...)在内存中生成了一个火腿包装盒(对象C)。- 构造时,我们把当前的
luxuryBreakfast(此时是 对象B 的地址)传给了它。所以,对象C 的肚子里装进了 对象B。 - 指针
luxuryBreakfast再次改变指向,最终指向了最外层的 对象C。
1 | 内存状态: |
📝 构造总结:顺序是 从内向外(先有原味煎饼,再套鸡蛋,最后套火腿)。最终你手里拿到的指针,永远是最外层那个包装盒的指针。
第二阶段:调用方法的过程(从外向内,再向外返回)
当我们调用 luxuryBreakfast->cost() 时:
**进入火腿层 (对象C)**:火腿类的
cost()方法被触发。它的代码是wrappedPancake->cost() + 2.0;。它不知道总价,必须先问它肚子里的兄弟。**进入鸡蛋层 (对象B)**:鸡蛋类的
cost()方法被触发。它的代码是wrappedPancake->cost() + 1.5;。它也不知道总价,继续向里问。**进入核心层 (对象A)**:基础煎饼的
cost()被触发。它直接返回了5.0。层层返回:
- 基础煎饼把
5.0传给鸡蛋层。 - 鸡蛋层拿到
5.0,加上自己的1.5,把6.5传给火腿层。 - 火腿层拿到
6.5,加上自己的2.0,最终返回8.5给调用者。
- 基础煎饼把
1 | 调用流程(像剥洋葱): |
第三阶段:析构(释放内存)的过程(链式反应)
这是最关键、也是最容易发生内存泄漏的地方。当我们吃完煎饼,执行 delete luxuryBreakfast; 时,发生了什么?
记住,luxuryBreakfast 现在指向的是最外层的 对象C(火腿包装盒)。
销毁火腿层 (对象C)
- 程序首先调用 对象C 的析构函数
~CondimentDecorator()。 - 这个析构函数里写了极其致命的一句代码:
delete wrappedPancake;。 - 对对象C来说,它肚子里的
wrappedPancake是 对象B(鸡蛋层) 的指针。 - 于是,这就触发了对 对象B 的
delete操作。
销毁鸡蛋层 (对象B)
- 程序进入 对象B 的析构函数
~CondimentDecorator()。 - 同样,它执行了
delete wrappedPancake;。 - 对对象B来说,它肚子里的指针是 对象A(基础煎饼)。
- 于是,触发了对 对象A 的
delete操作。
销毁核心层 (对象A)
- 程序进入 对象A 的析构函数
~BasePancake()。 - 对象A是最里面的核心,肚子里没有别人了。它安详地释放了属于自己的那块内存。
内存彻底清空
- 对象A 释放完毕。
- 对象B 随即释放完毕。
- 对象C 最终释放完毕。
1 | 析构流程(链式反应): |
📝 析构总结:顺序是 从外向内下达指令,从内向外依次销毁。这就像引爆了一根导火索,只要最外层的盒子被
delete,它就会自动去delete里面的盒子,里面的盒子再去delete更里面的盒子,直到最核心的对象被销毁,干干净净,没有任何内存泄漏。
🎨 4. 对象结构图解
让我们看看运行时对象是如何”套娃”的:
1 | 顾客 B 的豪华煎饼对象结构: |
📋 5. 优缺点与真实工业场景
✅ 优点
| 优点 | 说明 |
|---|---|
| 🧩 极度灵活,拒绝类爆炸 | 遵守合成复用原则(多用组合,少用继承)和开闭原则。加新配料只需写新装饰类,不影响老代码 |
| 🔄 动态可插拔 | 继承是编译时写死的,装饰模式是运行时像拼乐高一样随意组合 |
| 🎯 单一职责 | 每个装饰类只负责一个功能,代码清晰易维护 |
❌ 缺点
| 缺点 | 说明 |
|---|---|
| 📦 产生大量小对象 | 代码里多出一堆细碎的小包装类(鸡蛋类、火腿类) |
| 🐛 排错较困难 | 由于层层嵌套像洋葱,Debug 时可能要剥好几层才能找到核心对象 |
🏭 真实工业场景
在底层框架或流处理模块中,装饰模式用得极其频繁:
C++ I/O 流系统
1 | // 核心基础类 |
网络数据发送
| 层级 | 装饰器 | 功能 |
|---|---|---|
| 核心 | SocketStream |
基础网络流 |
| +1层 | BufferedStream |
缓冲优化 |
| +2层 | SSLStream |
加密传输 |
| +3层 | GzipStream |
压缩数据 |
🎯 6. 装饰模式 vs 继承
继承方式(类爆炸)
classDiagram
class Pancake {
+ cost()
}
class EggPancake {
+ cost()
}
class SausagePancake {
+ cost()
}
class EggSausagePancake {
+ cost()
}
class DoubleEggPancake {
+ cost()
}
Pancake <|-- EggPancake : 继承
Pancake <|-- SausagePancake : 继承
Pancake <|-- EggSausagePancake : 继承
Pancake <|-- DoubleEggPancake : 继承
装饰模式方式(灵活组合)
classDiagram
class IPancake {
+ cost()
}
class BasePancake {
+ cost()
}
class CondimentDecorator {
+ cost()
}
class EggDecorator {
+ cost()
}
class SausageDecorator {
+ cost()
}
IPancake <|.. BasePancake : 实现
IPancake <|.. CondimentDecorator : 实现
CondimentDecorator <|-- EggDecorator : 继承
CondimentDecorator <|-- SausageDecorator : 继承
CondimentDecorator o-- IPancake : 组合
| 对比项 | 继承 | 装饰模式 |
|---|---|---|
| 类数量 | 指数增长 📈 | 线性增长 📊 |
| 灵活性 | 编译时固定 🔒 | 运行时动态 🔄 |
| 开闭原则 | 违反 ❌ | 遵守 ✅ |
| 代码复用 | 通过继承 | 通过组合 |
🎓 总结
装饰模式是结构型模式中的经典之作,它完美诠释了:
🎯 “多用组合,少用继承” 的设计哲学
通过继承+组合的双重身份,装饰器既能伪装成被装饰对象,又能层层包装添加功能,实现了功能的动态叠加而不产生类爆炸。
📝 记忆口诀
“套娃装饰,层层叠加;组合继承,灵活优雅” 🎁
📝 本文通过 🎭 生活比喻、📊 UML类图、💻 完整代码案例,帮助你深入理解装饰模式。希望对你有所帮助!
- 本文作者: 迪丽惹Bug
- 本文链接: https://lyroom.github.io/2026/04/27/装饰模式详解/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!