Day 9:装饰器模式(Decorator Pattern)

C++17 · Qt6 · GoF 结构型模式 · 组合优于继承

① 今日主题

装饰器模式(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. 把请求转发给内层,保证调用链不断。

对调用者而言,它看到的始终是同一个接口——不知道也不关心对象被包了几层。行为却随着层数逐级增强:每多包一层,就多一份能力。

🧅 生活类比 1:给咖啡加料。一杯美式(原始对象)是"组件";加牛奶是一个装饰器,加糖是另一个装饰器,加奶油是第三个。你最终得到"美式+牛奶+糖+奶油",每一层加料都独立、可叠加、可调顺序,而且加料这件事完全发生在点单时(运行时),不是烘焙时就定死的。

🏠 生活类比 2:装修房子。毛坯房是原始对象;刷墙、铺地板、装吊灯、贴墙纸是四道独立工序(装饰器)。工序之间互不知晓、可任意叠加、可调先后。你绝不会为了"刷墙+铺地板"这个组合专门盖一种新户型——装饰器就是装修思维,继承是盖楼思维。

关键洞察:装饰器与被装饰者实现同一个接口。正因如此,装饰器可以替换被装饰者的位置(透明性),也可以继续被下一层装饰器包装(可叠加性)。这两个性质——透明性与可叠加性——是装饰器一切优势的根源。

④ UML / 角色关系

装饰器模式只有 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 的体现,也让装饰器能包装饰器;② 抽象装饰器的转发方法体几乎总是"原样转发",具体装饰器只重写自己关心的那一层。

⑤ 最小 C++ 示例(C++17)

用最经典的"消息装饰"演示:核心是普通文本,逐层附加「时间戳」「加粗」两种职责,全部在运行时动态组合。代码使用 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>

观察三个要点:

⑥ Qt 实战示例(Qt6 + C++17)

实战场景:给一个日志系统动态叠加能力——基础版输出到控制台,装饰器负责「加时间戳」「写文件」。功能可任意组合、可插拔,调用方只面对 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++ 示例为例,完整走一遍调用链(装饰器模式的标准运行时序):

  1. 构造核心对象:std::make_shared<PlainMessage>("Hello, Decorator!") 在堆上创建原始对象,引用计数为 1。
  2. 包第一层装饰器:TimestampDecorator(msg) 把 wrapped_ 指向核心对象(引用计数变 2)。装饰器自己也是 Message,所以它还能被继续包装。
  3. 包第二层装饰器:BoldDecorator(stamp) 把 wrapped_ 指向时间戳装饰器。此刻对象栈从外到内是:Bold → Timestamp → Plain。
  4. 发起调用:bold->text() 进入最外层——① BoldDecorator 先拼上 "<b>" 前缀;② 转发调用 wrapped_->text()(即 TimestampDecorator::text());③ 时间戳层拼上 "[2026-08-21 09:00] " 前缀,再转发到 PlainMessage::text();④ 核心对象返回原始文本;⑤ 控制权逐层返回,各层把自己的前缀/后缀拼装完成,最终输出 <b>[2026-08-21 09:00] Hello, Decorator!</b>。
  5. 析构:main 结束,三个 shared_ptr 出作用域,引用计数归零,对象按 Bold → Timestamp → Plain 从外向内依次析构——每个装饰器先析构自己,再释放内层,无泄漏、无悬空。

理解这条链的关键:每一层的 text() 都只做"自己的事 + 把球踢给内层"。调用是"从外到内"深入,返回是"从内到外"拼装,与递归调用栈完全一致。

⑧ 为什么这样设计

装饰器的每个设计决策都对应一条软件工程原则:

💡 一句话:装饰器把"功能的组合方式"从继承体系(编译期、树形、静态)搬到了对象链(运行时、线性、动态),用"一层层的包装"换来了"无限的扩展自由"。

⑨ 不使用会怎样(反例)

回到②里的继承方案,再看看不用装饰器时代码会腐化成什么样:

// ❌ 坏味道:功能开关堆进一个类,每加一个功能改一次这里
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;
    }
};

这三条路——类爆炸、if-else 膨胀、编译期固化——正是装饰器模式要消灭的三种腐化。

⑩ 何时使用

✅ 适合使用(3-5 个场景):

❌ 不适合使用(2-4 个场景):

⚠️ 过度设计提醒:装饰器最大的代价是"调试困难"——调用栈多、对象链长,断点要一层层跳。如果系统只有一两种固定功能,或功能永远不会在运行时变化,请直接使用继承或内联实现。装饰器是"为变化付费",没有变化就不该付费。

⑪ 与其他模式的区别

装饰器是最容易被混淆的结构型模式之一,重点辨析三个"近亲":

对比项装饰器 Decorator代理 Proxy适配器 Adapter组合 Composite
意图动态增强行为(加职责)控制访问(生命周期/权限/延迟)转换接口(让不兼容的协作)统一处理整体与部分
接口保持不变保持不变改变保持不变
子节点数一个(被包装者)一个(真实对象)一个(被适配者)多个(叶子+容器)
行为转发 + 附加职责转发 + 访问控制/懒加载接口翻译递归遍历

⑫ Qt 源码中的体现

Qt 框架里装饰器思想无处不在,最典型的三个:

此外,事件过滤器 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:① 功能组合固定、不会运行时变化——直接继承/直接实现更简单;② 热路径上对象创建频繁——包装开销不可接受;③ 需要按对象身份区分——装饰破坏身份;④ 接口过大——转发成本高,应优先拆分接口;⑤ 团队成员对模式不熟——装饰链会变成"看不懂的魔法洋葱",此时简单明确的代码优于炫技的模式。

⑭ 今日练习(15-30 分钟)

需求:网络数据流装饰器

定义一个数据流接口 DataStream(read()/write()),实现基础类 FileStream。然后用装饰器实现:

  • BufferedStream——附加缓冲能力(攒够 1KB 才真正写入);
  • EncryptedStream——附加加密能力(写入前 XOR 加密,读取时解密);
  • CompressStream——附加压缩能力(用简单的 RLE 行程编码示意即可,不必上 zlib)。

最终在 main() 中组合出「带缓冲的加密压缩文件流」,写入一段文本再读回,验证数据完整、调用顺序正确。

提示 1:每个装饰器只重写 write() 和 read() 两个方法,结构照抄今日的 MessageDecorator——附加逻辑 + 转发内层。

提示 2:加密/压缩实现越简单越好,练习的重点是包装结构与组合顺序,不是算法;组合时注意写入顺序与读取顺序要对称(写:压缩→加密→写;读:读→解密→解压)。

⑮ 今日总结

🧠 一句话记忆:装饰器 = 包洋葱——一层层把对象包起来,每一层"附加职责 + 转发内层",接口不变、行为无限叠加,用组合代替继承。

代码特征信号(看到这些,就该想到装饰器):

明日预告:Day 10 —— 外观模式(Facade Pattern)。今天给对象"加料",明天给子系统"开门":当一个模块背后藏着几十个类、几十个依赖时,外观模式用一个"门面类"把复杂性挡在身后。我们会看到 Qt 里 QFileDialog 如何一句话完成过去需要十几个类协作的操作,以及为什么说"外观是团队协作的润滑剂"。明天见!