装饰器模式(Decorator Pattern):在不修改原有类代码、不改变对外接口的前提下,通过包装(组合)的方式,动态地为单个对象附加新的职责。它被 GoF 称为"继承的一种更灵活的替代方案"。
一句话定义:把对象一层层"包起来",每一层在转发请求的同时附加自己的行为——加功能不用改类,用"穿衣服"代替"改基因"。
它是 GoF 23 种模式中第 9 个出场、结构型模式里的第 7 个(Singleton→Factory Method→Abstract Factory→Builder→Prototype→Adapter→Bridge→Composite→Decorator)。前 8 天我们学完了"创建型五人组"和三个结构型模式,今天进入结构型模式的深水区:如何在不触碰已有类的情况下,给对象"无限叠加"新能力。
先看没有模式时的"直觉方案":用继承给消息加功能。假设系统里有一个消息类,现在需要给它组合出「加密」「压缩」「加时间戳」「加粗」四种附加功能,于是我们为每种组合都写一个子类:
// ❌ 坏味道:继承方案——"组合爆炸"
class Message {
public:
virtual ~Message() = default;
virtual std::string text() const = 0;
};
class EncryptedMessage : public Message { /* 加密逻辑 */ };
class CompressedMessage : public Message { /* 压缩逻辑 */ };
class TimestampMessage : public Message { /* 时间戳 */ };
class BoldMessage : public Message { /* 加粗 */ };
// 两种功能组合:又要 6 个子类……
class EncryptedCompressedMessage : public Message { /* 加密+压缩 */ };
class EncryptedTimestampMessage : public Message { /* 加密+时间戳 */ };
class CompressedBoldMessage : public Message { /* 压缩+加粗 */ };
// …… 四种功能就要 2^4 = 16 个子类
问题立刻暴露:
从 SOLID 看:
装饰器模式正是为消灭类爆炸而生:把"功能"从"类"里拆出来,变成一个个可以任意嵌套、任意组合的包装层。
装饰器的本质是俄罗斯套娃 / 洋葱模型:最里面是原始对象,外面每一层都是一个"装饰器"。每一层装饰器只做两件事:
对调用者而言,它看到的始终是同一个接口——不知道也不关心对象被包了几层。行为却随着层数逐级增强:每多包一层,就多一份能力。
🧅 生活类比 1:给咖啡加料。一杯美式(原始对象)是"组件";加牛奶是一个装饰器,加糖是另一个装饰器,加奶油是第三个。你最终得到"美式+牛奶+糖+奶油",每一层加料都独立、可叠加、可调顺序,而且加料这件事完全发生在点单时(运行时),不是烘焙时就定死的。
🏠 生活类比 2:装修房子。毛坯房是原始对象;刷墙、铺地板、装吊灯、贴墙纸是四道独立工序(装饰器)。工序之间互不知晓、可任意叠加、可调先后。你绝不会为了"刷墙+铺地板"这个组合专门盖一种新户型——装饰器就是装修思维,继承是盖楼思维。
关键洞察:装饰器与被装饰者实现同一个接口。正因如此,装饰器可以替换被装饰者的位置(透明性),也可以继续被下一层装饰器包装(可叠加性)。这两个性质——透明性与可叠加性——是装饰器一切优势的根源。
装饰器模式只有 4 个角色,结构非常对称:
┌──────────────────────────┐
│ <<interface>> │
│ Component │
│ + operation() │
└────────────┬─────────────┘
▲
┌───────────────┴───────────────┐
│ │
┌────────────┴────────────┐ ┌──────────────┴─────────────┐
│ ConcreteComponent │ │ Decorator │
│ (被装饰的原始对象) │ │ - wrapped: Component │
│ + operation() │ │ + operation() │
└─────────────────────────┘ │ // 转发给 wrapped │
└──────────────┬─────────────┘
▲
┌───────────────┴───────────────┐
│ ConcreteDecorator │
│ + operation() │
│ // 附加职责 + 转发内层 │
└───────────────────────────────┘
| 角色 | 职责 | 本日示例 |
|---|---|---|
| Component(抽象组件) | 定义统一接口,是"可被装饰"的资格证 | Message(纯虚 text()) |
| ConcreteComponent(具体组件) | 被装饰的原始对象,实现基础行为 | PlainMessage |
| Decorator(抽象装饰器) | 持有 Component 引用,默认透明转发,为子类提供挂载点 | MessageDecorator |
| ConcreteDecorator(具体装饰器) | 在转发前后附加自己的职责,可无限叠加 | TimestampDecorator、BoldDecorator |
注意两个设计要点:① 抽象装饰器里持有的是抽象 Component 引用(不是具体类),这是 DIP 的体现,也让装饰器能包装饰器;② 抽象装饰器的转发方法体几乎总是"原样转发",具体装饰器只重写自己关心的那一层。
用最经典的"消息装饰"演示:核心是普通文本,逐层附加「时间戳」「加粗」两种职责,全部在运行时动态组合。代码使用 std::shared_ptr 管理对象生命周期,杜绝悬空指针与内存泄漏。
#include <iostream>
#include <memory> // std::shared_ptr / std::make_shared
#include <string>
// ========== ① 抽象组件 Component:定义统一接口 ==========
class Message {
public:
virtual ~Message() = default; // 虚析构:基类指针安全删除派生类
virtual std::string text() const = 0; // 纯虚:所有消息都必须能取文本
};
// ========== ② 具体组件 ConcreteComponent:原始对象 ==========
class PlainMessage : public Message {
std::string content_;
public:
explicit PlainMessage(std::string content) : content_(std::move(content)) {}
std::string text() const override { return content_; } // 基础行为:原样返回
};
// ========== ③ 抽象装饰器 Decorator:持有内层对象并透明转发 ==========
class MessageDecorator : public Message {
protected:
std::shared_ptr<Message> wrapped_; // 指向被包装的内层对象
public:
explicit MessageDecorator(std::shared_ptr<Message> m) : wrapped_(std::move(m)) {}
// 关键点:默认原样转发,接口保持透明——子类只需重写要附加职责的那一层
std::string text() const override { return wrapped_->text(); }
};
// ========== ④ 具体装饰器 A:附加时间戳(转发前加前缀) ==========
class TimestampDecorator : public MessageDecorator {
public:
using MessageDecorator::MessageDecorator; // 继承构造函数
std::string text() const override {
return "[2026-08-21 09:00] " + wrapped_->text(); // 附加职责 + 转发
}
};
// ========== ④ 具体装饰器 B:附加加粗标记(前后包裹) ==========
class BoldDecorator : public MessageDecorator {
public:
using MessageDecorator::MessageDecorator;
std::string text() const override {
return "<b>" + wrapped_->text() + "</b>"; // 前后包裹 + 转发
}
};
int main() {
// 运行时动态组合:从内到外 核心 → 时间戳 → 加粗
auto msg = std::make_shared<PlainMessage>("Hello, Decorator!");
auto stamp = std::make_shared<TimestampDecorator>(msg);
auto bold = std::make_shared<BoldDecorator>(stamp);
// 调用者眼里只有 Message 接口,行为却被逐层增强
std::cout << bold->text() << std::endl;
return 0; // shared_ptr 自动析构:Bold → Timestamp → Plain,零泄漏
}
程序输出:
<b>[2026-08-21 09:00] Hello, Decorator!</b>
观察三个要点:
EmojiDecorator),已有的类一个都不用改——OCP 达成。BoldDecorator 和 TimestampDecorator 交换位置,输出就变成 [时间戳] <b>...</b>。make_shared 的事,编译期零成本。实战场景:给一个日志系统动态叠加能力——基础版输出到控制台,装饰器负责「加时间戳」「写文件」。功能可任意组合、可插拔,调用方只面对 ILogger 接口。工程只需 QtCore,命令行即可编译运行。
ilogger.h —— 抽象组件
#pragma once
#include <QString>
// 抽象组件:日志输出接口,一切装饰的挂载点
class ILogger {
public:
virtual ~ILogger() = default;
virtual void write(const QString& msg) = 0;
};
consolelogger.h —— 具体组件(原始对象)
#pragma once
#include "ilogger.h"
#include <QDebug>
// 具体组件:把日志打到控制台(stderr)
class ConsoleLogger : public ILogger {
public:
void write(const QString& msg) override {
qInfo().noquote() << msg; // noquote:避免 QString 被加引号
}
};
loggerdecorator.h —— 抽象装饰器(透明转发)
#pragma once
#include "ilogger.h"
#include <memory>
// 抽象装饰器:持有内层 ILogger,默认透明转发
class LoggerDecorator : public ILogger {
public:
explicit LoggerDecorator(std::shared_ptr<ILogger> inner)
: inner_(std::move(inner)) {}
void write(const QString& msg) override { inner_->write(msg); }
protected:
std::shared_ptr<ILogger> inner_; // shared_ptr:内层对象安全析构,无泄漏
};
timestamplogger.h —— 具体装饰器:附加时间戳
#pragma once
#include "loggerdecorator.h"
#include <QDateTime>
// 具体装饰器:转发前附加当前时间戳
class TimestampLogger : public LoggerDecorator {
public:
using LoggerDecorator::LoggerDecorator; // 继承构造函数
void write(const QString& msg) override {
const QString stamp =
QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
inner_->write("[" + stamp + "] " + msg); // 附加职责 + 转发
}
};
filelogger.h —— 具体装饰器:同时落盘
#pragma once
#include "loggerdecorator.h"
#include <QFile>
#include <QTextStream>
// 具体装饰器:转发前先把日志追加写入文件(RAII 管理 QFile)
class FileLogger : public LoggerDecorator {
public:
FileLogger(std::shared_ptr<ILogger> inner, const QString& filePath)
: LoggerDecorator(std::move(inner)) {
file_.setFileName(filePath);
file_.open(QIODevice::Append | QIODevice::Text);
}
~FileLogger() override { if (file_.isOpen()) file_.close(); }
void write(const QString& msg) override {
QTextStream out(&file_);
out << msg << "\n"; // 附加职责:写文件
inner_->write(msg); // 转发:交给内层继续处理
}
private:
QFile file_;
};
main.cpp —— 运行时组合三件套
#include <memory>
#include "consolelogger.h"
#include "timestamplogger.h"
#include "filelogger.h"
int main() {
// 从内到外:控制台 → 加时间戳 → 落盘
auto logger = std::make_shared<FileLogger>(
std::make_shared<TimestampLogger>(
std::make_shared<ConsoleLogger>()),
"app.log");
// 调用方完全无感知地获得"时间戳 + 双写"全部增强
logger->write("application started");
logger->write("user logged in");
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(decorator_logger LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core)
qt_standard_project_setup()
qt_add_executable(decorator_logger
main.cpp
ilogger.h
consolelogger.h
loggerdecorator.h
timestamplogger.h
filelogger.h
)
target_link_libraries(decorator_logger PRIVATE Qt6::Core)
运行输出(控制台 + app.log 内容相同):
[2026-08-21 09:00:15] application started
[2026-08-21 09:00:15] user logged in
🔍 重点:Qt 自身就是装饰器的重度用户。
① QGraphicsEffect 家族——QGraphicsDropShadowEffect(阴影)、QGraphicsBlurEffect(模糊)、QGraphicsOpacityEffect(半透明)、QGraphicsColorizeEffect(着色)全部继承自抽象类 QGraphicsEffect,它们"装饰"的是控件的绘制过程:挂到 QWidget::setGraphicsEffect() 后,控件原本的绘制结果先经过效果层再上屏,控件自身代码零改动;效果之间还可用 setSource() 链式叠加,与装饰器嵌套完全同构。新增一种视觉效果 = 新增一个 QGraphicsEffect 子类。
② QSortFilterProxyModel——包装一个源 QAbstractItemModel,重写 rowCount()/data()/sort()/filterAcceptsRow(),在透明转发源模型数据的同时附加排序、过滤、行列映射职责;一个代理还能再包另一个代理形成链(如 排序代理 → 过滤代理 → 视图)。它是"装饰器 + 代理"的混合体,是 Qt 模型视图体系里最值得反复品读的类之一。
以最小 C++ 示例为例,完整走一遍调用链(装饰器模式的标准运行时序):
std::make_shared<PlainMessage>("Hello, Decorator!") 在堆上创建原始对象,引用计数为 1。TimestampDecorator(msg) 把 wrapped_ 指向核心对象(引用计数变 2)。装饰器自己也是 Message,所以它还能被继续包装。BoldDecorator(stamp) 把 wrapped_ 指向时间戳装饰器。此刻对象栈从外到内是:Bold → Timestamp → Plain。bold->text() 进入最外层——① BoldDecorator 先拼上 "<b>" 前缀;② 转发调用 wrapped_->text()(即 TimestampDecorator::text());③ 时间戳层拼上 "[2026-08-21 09:00] " 前缀,再转发到 PlainMessage::text();④ 核心对象返回原始文本;⑤ 控制权逐层返回,各层把自己的前缀/后缀拼装完成,最终输出 <b>[2026-08-21 09:00] Hello, Decorator!</b>。main 结束,三个 shared_ptr 出作用域,引用计数归零,对象按 Bold → Timestamp → Plain 从外向内依次析构——每个装饰器先析构自己,再释放内层,无泄漏、无悬空。理解这条链的关键:每一层的 text() 都只做"自己的事 + 把球踢给内层"。调用是"从外到内"深入,返回是"从内到外"拼装,与递归调用栈完全一致。
装饰器的每个设计决策都对应一条软件工程原则:
Component 抽象接口这一条耦合。装饰器不知道内层是核心对象还是另一个装饰器——这正是"能包装饰器"的根本原因。Message/ILogger 抽象,而不是任何具体装饰器;具体装饰器也依赖抽象而非具体——整个体系都"面向接口编程"。💡 一句话:装饰器把"功能的组合方式"从继承体系(编译期、树形、静态)搬到了对象链(运行时、线性、动态),用"一层层的包装"换来了"无限的扩展自由"。
回到②里的继承方案,再看看不用装饰器时代码会腐化成什么样:
EncryptedCompressedTimestampedBoldUpperCaseMessage 这种"火车残骸",IDE 补全都救不了你。// ❌ 坏味道:功能开关堆进一个类,每加一个功能改一次这里
enum class Features { None, Encrypt, Compress, EncryptAndCompress, /* … */ };
class Message {
Features features_;
public:
std::string text() const {
std::string t = content_;
if (features_ == Features::Encrypt) t = encrypt(t);
else if (features_ == Features::Compress) t = compress(t);
else if (features_ == Features::EncryptAndCompress) {
t = encrypt(t);
t = compress(t);
}
// …每新增一种组合,这里就多一个 else if
return t;
}
};
else if 链越来越长;逻辑交织导致测试用例爆炸、回归风险升高;运行时想换组合只能改枚举值,且改完所有调用点都要跟进。这三条路——类爆炸、if-else 膨胀、编译期固化——正是装饰器模式要消灭的三种腐化。
✅ 适合使用(3-5 个场景):
❌ 不适合使用(2-4 个场景):
⚠️ 过度设计提醒:装饰器最大的代价是"调试困难"——调用栈多、对象链长,断点要一层层跳。如果系统只有一两种固定功能,或功能永远不会在运行时变化,请直接使用继承或内联实现。装饰器是"为变化付费",没有变化就不该付费。
装饰器是最容易被混淆的结构型模式之一,重点辨析三个"近亲":
| 对比项 | 装饰器 Decorator | 代理 Proxy | 适配器 Adapter | 组合 Composite |
|---|---|---|---|---|
| 意图 | 动态增强行为(加职责) | 控制访问(生命周期/权限/延迟) | 转换接口(让不兼容的协作) | 统一处理整体与部分 |
| 接口 | 保持不变 | 保持不变 | 改变 | 保持不变 |
| 子节点数 | 一个(被包装者) | 一个(真实对象) | 一个(被适配者) | 多个(叶子+容器) |
| 行为 | 转发 + 附加职责 | 转发 + 访问控制/懒加载 | 接口翻译 | 递归遍历 |
Qt 框架里装饰器思想无处不在,最典型的三个:
QGraphicsEffect + QGraphicsDropShadowEffect / QGraphicsBlurEffect / QGraphicsOpacityEffect / QGraphicsColorizeEffect。它们不修改控件任何代码,只"装饰"绘制输出;setSource() 支持效果链式嵌套——QGraphicsDropShadowEffect 里再挂 QGraphicsBlurEffect,与装饰器层层包装一一对应。阅读源码时注意它内部的 sourceChanged/draw() 机制:效果层拦截绘制事件、附加效果、再调用源对象的绘制,正是"附加职责 + 转发内层"。QAbstractItemModel,重写 rowCount()/columnCount()/data()/index()/parent() 透明转发,并在 filterAcceptsRow()/lessThan() 处附加过滤与排序。它的基类 QAbstractProxyModel 就是"抽象装饰器",一个代理可再包一个代理形成多层映射链。这是 Qt 模型视图架构里理解"包装"二字的最佳教材。QTextStream 包装 QIODevice(附加格式化与缓冲)、QDataStream 包装 QIODevice(附加二进制序列化)、QBuffer/QTcpSocket 各自装饰不同的数据源——"读/写能力"被一层层套在底层的字节通道上,与装饰器的流式叠加完全同构。此外,事件过滤器 QObject::installEventFilter() 也带有装饰/职责链色彩(在转发事件前后附加处理),但它的职责模型更接近职责链模式,Day 13 会单独拆解。
Q1:装饰器模式和继承有什么区别?为什么说装饰器是继承的替代方案?
A:继承是编译期静态关系,子类在编译时确定、无法运行时改变,且功能组合会带来 2^N 的类爆炸;装饰器把功能拆成独立的包装层,在运行时通过对象组合动态装配——新增功能只需新增一个装饰器类,已有类零修改,组合数从 2^N 降为 N+2。继承表达"本质变体"(Dog 是 Animal 的一种),装饰器表达"临时加料"(给任意对象动态叠加能力)。
Q2:装饰器模式和代理模式的区别?
A:两者结构几乎相同(都持有同接口对象并转发),区别在意图:装饰器添加职责(增强行为,如加时间戳、加密),代理控制访问(管理生命周期、权限校验、延迟加载,行为本身不变)。一句话:装饰器改变"做什么",代理控制"怎么到达"。Qt 的 QSortFilterProxyModel 是二者混合的实例。
Q3:装饰器模式有什么缺点?
A:① 小对象层层包装增加对象数量与调用开销(热路径有性能损耗);② 调试困难,调用栈深、对象链长,断点要逐层跳;③ 破坏对象身份,外层装饰器 != 内层对象,按指针/引用比较时失效;④ 接口庞大时抽象装饰器的透明转发代码冗长;⑤ 若装饰器暴露了内部对象的方法(如 getWrapped()),会破坏封装。
Q4:C++ 实现装饰器时如何保证内存安全?
A:用 std::shared_ptr<Component> 持有内层对象:装饰器链共享所有权,引用计数自动管理,析构按"外层→内层"顺序进行,无悬空指针、无泄漏;被装饰对象接口必须有虚析构函数;装饰器构造用 std::move 转移所有权避免多余拷贝;尽量用 make_shared 一次性分配。若明确单所有权且无环,也可用 std::unique_ptr 更轻量。
Q5:什么时候不该用装饰器模式?
A:① 功能组合固定、不会运行时变化——直接继承/直接实现更简单;② 热路径上对象创建频繁——包装开销不可接受;③ 需要按对象身份区分——装饰破坏身份;④ 接口过大——转发成本高,应优先拆分接口;⑤ 团队成员对模式不熟——装饰链会变成"看不懂的魔法洋葱",此时简单明确的代码优于炫技的模式。
需求:网络数据流装饰器
定义一个数据流接口 DataStream(read()/write()),实现基础类 FileStream。然后用装饰器实现:
BufferedStream——附加缓冲能力(攒够 1KB 才真正写入);EncryptedStream——附加加密能力(写入前 XOR 加密,读取时解密);CompressStream——附加压缩能力(用简单的 RLE 行程编码示意即可,不必上 zlib)。最终在 main() 中组合出「带缓冲的加密压缩文件流」,写入一段文本再读回,验证数据完整、调用顺序正确。
提示 1:每个装饰器只重写 write() 和 read() 两个方法,结构照抄今日的 MessageDecorator——附加逻辑 + 转发内层。
提示 2:加密/压缩实现越简单越好,练习的重点是包装结构与组合顺序,不是算法;组合时注意写入顺序与读取顺序要对称(写:压缩→加密→写;读:读→解密→解压)。
代码特征信号(看到这些,就该想到装饰器):
wrapped_);make_shared/make_unique 的层层嵌套调用。明日预告:Day 10 —— 外观模式(Facade Pattern)。今天给对象"加料",明天给子系统"开门":当一个模块背后藏着几十个类、几十个依赖时,外观模式用一个"门面类"把复杂性挡在身后。我们会看到 Qt 里 QFileDialog 如何一句话完成过去需要十几个类协作的操作,以及为什么说"外观是团队协作的润滑剂"。明天见!