【设计模式系列 (九) 】装饰器模式

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页 :YYYing.

⭐️设计模式系列专栏:设计模式系列

系列上期内容:【设计模式系列 (八) 】组合模式

系列下期内容:暂无


目录

[1. 概述](#1. 概述)

[2. 结构](#2. 结构)

[3. 透明与半透明装饰](#3. 透明与半透明装饰)

[3.1 透明装饰模式](#3.1 透明装饰模式)

[3.2 半透明装饰模式](#3.2 半透明装饰模式)

[4. 实现:巨魔对战(decorator/)](#4. 实现:巨魔对战(decorator/))

[4.1 场景](#4.1 场景)

[4.2 Component:巨魔接口](#4.2 Component:巨魔接口)

[4.3 ConcreteComponent:普通巨魔](#4.3 ConcreteComponent:普通巨魔)

[4.4 Decorator:抽象的转发层](#4.4 Decorator:抽象的转发层)

[4.5 ConcreteDecorator:两个具体装饰器](#4.5 ConcreteDecorator:两个具体装饰器)

[4.6 Client:装饰双方](#4.6 Client:装饰双方)

[5. 优缺点与适用环境](#5. 优缺点与适用环境)

[6. 面试专题](#6. 面试专题)

[6.1 开场题:说说装饰模式吧](#6.1 开场题:说说装饰模式吧)

[6.2 必问:为什么用装饰模式而不是继承?](#6.2 必问:为什么用装饰模式而不是继承?)

[6.3 必问:装饰器 vs 适配器 vs 代理](#6.3 必问:装饰器 vs 适配器 vs 代理)

[6.4 高频追问清单](#6.4 高频追问清单)

[6.5 什么时候不该用装饰模式](#6.5 什么时候不该用装饰模式)

结语


一张照片,本身没变,套个相框就多了防潮的功能;小相框外面还能再套一个大相框,一层层加下去。装饰模式做的就是这件事:在不改变原对象的前提下,给它动态地加职责。

本文代码取自 design-pattern-cpp 仓库的 design-pattern/decorator/,使用 C++11。为阅读方便,片段省略了头文件保护宏。


1. 概述

Wikipedia :In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object , either statically or dynamically, without affecting the behavior of other objects from the same class.The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern.

在面向对象编程中,装饰器模式是一种设计模式,它允许将行为静态或动态地添加到单个对象中,而不会影响同一类中其他对象的行为。装饰器模式对于遵守单一责任原则通常是有用的,因为它允许将功能划分到具有独特关注区域的类之间。

GoF :Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

动态的给一个对象增加一些额外的职责。就扩展功能而言,装饰器模式提供了一种比使用子类更灵活的 方式。

三个关键词:

① 动态加职责,且只影响这一个对象

这是和继承最本质的区别。继承是编译期 决定的、影响整类 对象的;装饰是运行期 决定的、影响单个 对象。同一个 SimpleTroll 类,A 实例套上攻击装饰器,B 实例保持原样,互不干扰。

② 继承的替代方案

"扩展功能"这件事继承也能做,但代价是类爆炸 :m 种基础功能 + n 种扩展,用继承要写 m×n 个子类。装饰模式用组合替换继承,m + n 个类就够了,而且能任意排列组合。

③ 别名叫 Wrapper(包装模式)

注意:适配器模式的别名也叫 Wrapper 。别名重合恰恰说明这两个模式长得很像(都是"包一层、转发调用"),区分它们只能看意图,见 6.3。

装饰模式属于结构型模式(Structural Pattern)。


2. 结构

角色 职责
Component(抽象构件) 具体构件和抽象装饰类的共同父类,声明业务方法。它的存在让客户端能一致地处理装饰前后的对象
ConcreteComponent(具体构件) 被装饰的原始对象,实现 Component 声明的方法
Decorator(抽象装饰类) Component 的子类,持有一个 Component 引用,把请求转发给它;具体装饰逻辑留给子类
ConcreteDecorator(具体装饰类) 向构件添加新职责,可以在转发前后插入自己的逻辑

图里最关键的一点:Decorator 既"是一个 Component"(继承),又"持有一个 Component"(组合)。

  • 继承 Component → 保证装饰后的对象和原对象接口一致,客户端感觉不到区别;

  • 持有 Component → 保证能在转发调用的前后 插入新逻辑,而且持的是抽象类型,所以能一层套一层。

一个装饰器套在另一个装饰器上,每个都只知道"我包着一个 Component"------这就是它支持多层嵌套的原因,也是继承做不到的。


3. 透明与半透明装饰

这是装饰模式特有的考点。

3.1 透明装饰模式

要求客户端完全面向抽象编程 ,装饰前后的对象都声明为 Component*:

cpp 复制代码
Component* c1 = new ConcreteComponent();
Component* c2 = new ConcreteDecorator(c1);   // ★ 声明为 Component*

客户端分不清手里是原对象还是装饰后的对象,好处是:

  • 装饰前后的对象一视同仁;

  • 可以多次装饰 :new DecoratorA(new DecoratorB(new DecoratorA(comp)))。

代价是调不到新增的方法 ------因为 Component 里根本没声明它们。

3.2 半透明装饰模式

如果确实需要调用新增方法,只能用具体装饰类型来声明:

cpp 复制代码
Component* c1 = new ConcreteComponent();
ConcreteDecorator* c2 = new ConcreteDecorator(c1);   // ★ 声明为具体装饰类型
c2->operation();      // 继承来的方法
c2->addFunc();        // ★ 新增的方法,透明模式下调不到
透明装饰 半透明装饰
装饰后对象的声明类型 Component* 具体装饰类 *
能否多次装饰 ✅ 能 ❌ 不能(链断了,第二次装饰拿不到完整接口)
能否调用新增方法 ❌ 不能 ✅ 能
客户端复杂度 低 需要区别对待装饰前后的对象

一句话 :透明 = "当成普通 Component 用 ",半透明 = "我知道你被装饰过,还要用你的新功能 "。设计上优先透明,因为它才真正解耦。


4. 实现:巨魔对战(decorator/)

4.1 场景

巨魔(Troll)在对战中,可以在不新建巨魔的前提下,动态获得武器或防具加成------"总不可能把巨魔杀了重造一个吧"。这正是装饰模式的用武之地。

4.2 Component:巨魔接口

cpp 复制代码
namespace decorator
{
    class Troll
    {
    public:
        virtual int attackTarget(Troll* troll) = 0;   // 攻击目标,返回伤害值
        virtual void hit(int hitval) = 0;             // 受到伤害
        virtual int getAp() = 0;  virtual void setAp(int ap) = 0;   // 攻击力
        virtual int getDp() = 0;  virtual void setDp(int dp) = 0;   // 防御力
        virtual int getHp() = 0;
        virtual std::string getName() = 0;
    };
}

4.3 ConcreteComponent:普通巨魔

cpp 复制代码
class SimpleTroll final : public Troll      // decorator/SimpleTroll.h
{
private:
    int hp; int attack; int defense; std::string name;
public:
    SimpleTroll(std::string name, int hp, int ap, int dp);
​
    int attackTarget(Troll* target) override
    {
        int hitval = attack - target->getDp();
        if (hitval <= 0) hitval = 1;        // 没破防,强制伤害 1
        target->hit(hitval);
        return hitval;                       // ★ 返回值是装饰器的关键钩子
    }
    // ... hit / getAp / setAp / getDp / setDp / getHp / getName
};

注意 attackTarget 返回了伤害值------具体装饰器就靠这个返回值判断"有没有破防",从而决定要不要加攻击力。

4.4 Decorator:抽象的转发层

cpp 复制代码
class DecoratorTroll : public Troll          // decorator/DecoratorTroll.h
{
private:
    Troll* troll;                            // ★ 持有一个抽象构件
public:
    DecoratorTroll(Troll* troll);
​
    int attackTarget(Troll* target) override { return troll->attackTarget(target); }
    void hit(int hitval) override            { troll->hit(hitval); }
    int getAp() override                     { return troll->getAp(); }
    void setAp(int ap) override              { troll->setAp(ap); }
    // ... 其余方法一律原样转发
};

DecoratorTroll 本身一点装饰都不做 ,它的全部价值是把接口原样转发给被装饰对象,给子类留出覆写点。这一层是"透传默认实现",子类只覆盖自己关心的那一两个方法。

4.5 ConcreteDecorator:两个具体装饰器

加攻击力------连续 5 次没破防,就掏屠龙刀:

cpp 复制代码
class AddAttackTroll : public DecoratorTroll   // decorator/AddAttackTroll.h
{
private:
    int notdefense;      // 未破防的累计次数
    bool isAdd;          // 是否已经加过(只加一次)
public:
    AddAttackTroll(Troll* troll);
    int attackTarget(Troll* target) override;
};
​
int decorator::AddAttackTroll::attackTarget(Troll* target)
{
    int hitval = DecoratorTroll::attackTarget(target);   // ★ 先转发,拿到结果
    if (hitval == 1 && !isAdd)                            // ★ 在结果上做判断
    {
        notdefense++;
        if (notdefense >= 5)
        {
            setAp(getAp() + 100);                         // ★ 调用被装饰对象的方法
            std::cout << getName() << "手持屠龙刀,攻击力提升了" << 100 << "点" << std::endl;
            isAdd = true;
        }
    }
    return hitval;
}

加防御力------血量掉到 10% 以下,就穿上黄金甲:

cpp 复制代码
class AddDefenseTroll : public DecoratorTroll   // decorator/AddDefenseTroll.h
{
private:
    int percetHp;                                 // 仓库原拼写
    bool isAdd;
    const int maxPercent = 0.1;
public:
    AddDefenseTroll(Troll* troll);
    void hit(int hitval) override;                // ★ 覆写的是 hit,不是 attackTarget
};
​
void decorator::AddDefenseTroll::hit(int hitval)
{
    DecoratorTroll::hit(hitval);                        // ★ 先转发,扣完血
    if (getHp() > 0 && !isAdd && getHp() < percetHp)    // ★ 再判断残血
    {
        setDp(getDp() + 100);
        std::cout << getName() << "穿上了黄金甲,获得了" << 100 << "点防御力" << std::endl;
        isAdd = true;
    }
}

两个装饰器给出了两种典型的插桩位置 :AddAttackTroll 在转发之后、基于返回值 做判断;AddDefenseTroll 在转发之后、基于对象状态做判断。装饰器的自由度就在这里------转发前、转发后、甚至替换掉转发逻辑,都可以。

4.6 Client:装饰双方

cpp 复制代码
int main()
{
    // 普通巨魔 VS 附加武器的巨魔
    SimpleTroll ddg1("ddg", 300, 20, 30);
    SimpleTroll yh("yh", 300, 20, 10);
    AddAttackTroll atYh(&yh);                     // ★ 给yh套上"加攻击力"装饰
    do {
        ddg1.attackTarget(&atYh);                 // 装饰后的对象照样能当 Troll* 用
        if (atYh.getHp() <= 0) break;
        atYh.attackTarget(&ddg1);
        if (ddg1.getHp() <= 0) break;
    } while (1);
​
    // 普通巨魔 VS 附加防具的巨魔
    SimpleTroll ddg2("ddg", 900, 40, 5);
    SimpleTroll tlj("tlj", 300, 40, 5);
    AddDefenseTroll adTlj(&tlj);                  // ★ 给tlj套上"加防御力"装饰
    AddAttackTroll  apDdg(&ddg2);                 // ★ ddg也套上攻击装饰
    // ... 同上循环
}

客户端拿到 &atYh 后照常当 Troll* 传给 attackTarget------装饰后的对象和被装饰对象在使用上没有任何区别,这就是第 3 节说的"透明"。


5. 优缺点与适用环境

主要优点

  • 比继承灵活:扩展对象功能不会导致类个数急剧增加(m + n 而非 m × n);

  • 动态扩展:运行期决定要不要装饰、装饰几层,还能通过配置文件选择具体装饰类;

  • 可以多次装饰:不同具体装饰类排列组合,能拼出大量行为组合;

  • 符合开闭原则 :具体构件类和具体装饰类可以独立变化,新增装饰不需要改动原有类库。

主要缺点

  • 产生大量小对象:这些对象的区别只在"连接方式",大量小对象占用更多系统资源,影响性能;

  • 排错困难 :多层装饰后,调试要找的错误可能需要逐级排查;

  • 比继承更易出错:灵活性换来的是更高的心智负担。

适用环境

  • 需要在不影响其他对象 的前提下,以动态、透明的方式给单个对象添加职责;

  • 不能采用继承,或采用继承不利于扩展和维护时。具体有两类情况:

    • 系统里存在大量独立的扩展 ,为每种扩展及其组合生成子类会导致子类数量爆炸;

    • 类被定义为不能被继承 (如 final 类)。

使用注意事项:

  • 尽量保持装饰类和被装饰类的接口相同,让客户端能一致对待------也就是优先用透明装饰;

  • 尽量让 ConcreteComponent 是个"轻"类,别把太多行为塞进去,扩展交给装饰器;

  • 如果只有一个具体构件类,抽象装饰类可以直接作为它的子类(省掉 Component 抽象层)。


6. 面试专题

6.1 开场题:说说装饰模式吧

按"概念 → 角色 → 适用环境 → 优缺点 → 项目里怎么用"的顺序答:

概念 :装饰模式动态地给一个对象增加一些额外的职责 ,同时不修改它原来的结构。装饰类和被装饰类可以独立发展、互不耦合。它是继承的一种替代模式,通过对象间的关联关系取代类之间的继承关系来扩展功能。属于结构型模式,别名 Wrapper。

角色:抽象构件 Component 是具体构件和抽象装饰类的共同父类;具体构件 ConcreteComponent 是被装饰的原始对象;抽象装饰类 Decorator 持有 Component 引用并转发请求;具体装饰类 ConcreteDecorator 负责添加新职责。

适用环境:在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责;或者不能采用继承、继承不利于扩展维护时(大量独立扩展导致子类爆炸,或者类是 final 的)。

可扩展:接优缺点、接透明/半透明、接和适配器/代理的辨析(见下)。

项目里怎么用(这半句常常是加分项):

  • 自定义 UI 组件 :给基础控件动态叠加边框、滚动条、阴影等效果,而不是为每种组合派生新类。Java 的 java.io 流是教科书用例------new BufferedReader(new InputStreamReader(new FileInputStream(...)));

  • 对数据进行包装:给数据源叠加缓存、压缩、加密、校验等职责,各层独立可插拔。

6.2 必问:为什么用装饰模式而不是继承?

继承 装饰模式
决定时机 编译期,静态 运行期,动态
影响范围 整个类的所有对象 单个对象
扩展代价 m 种功能 × n 种扩展 → m×n 个子类 m + n 个类,排列组合
能否叠加 子类层级越深越僵化 可以任意层嵌套
能否撤销 不能 可以(不套那层就行)
对原类的侵入 需要派生,可能改到原有结构 零修改,符合开闭原则

一句话 :继承是"生下来就带着 ",装饰是"穿上去的,能脱能换"。

6.3 必问:装饰器 vs 适配器 vs 代理

这三个模式结构几乎一模一样 ------都是"实现同一个接口,在方法里转发给另一个对象"(所以适配器和装饰器的别名都叫 Wrapper)。区分它们只能靠意图:

装饰器 Decorator 适配器 Adapter 代理 Proxy
意图 增强功能 转换接口 控制访问
接口 与目标同一个 转换成另一个 与目标同一个
是否改变行为 ✅ 增强,可叠加多层 ❌ 只做转换 ✅ 增加控制逻辑
目标从哪来 外部传入 外部传入(已有的、类型不同) 自己创建或持有
客户端是否知情 通常不知情(透明) 感知不到 Adaptee 通常知道自己在用代理
典型用途 IO 流、加密、压缩、UI 效果 兼容第三方库、遗留代码 延迟加载、权限校验、日志、缓存、AOP

一句话记忆:

适配器管"接口对不对 ",装饰器管"功能够不够 ",代理管"能不能给你用"。

再补一句更锋利的判断:适配器只转换、不增强;装饰器只增强、不转换;代理增强的是"访问控制",而不是"功能本身"。

6.4 高频追问清单

追问 答法
装饰模式属于哪一类? 结构型模式,别名 Wrapper(包装模式)
透明装饰和半透明装饰的区别? 透明:装饰前后都声明为 Component*,能多次装饰但调不到新增方法;半透明:用具体装饰类型声明,能调新增方法但不能多次装饰。设计上优先透明。见第 3 节
装饰器能嵌套多少层? 理论上任意层。每层只依赖抽象 Component,所以可以一直套下去
装饰器会不会改变原对象? 不会。装饰器持有的是原对象的引用,只在外层加行为,原对象本身没有变化
装饰器里的 Component* 谁来释放? 装饰器不负责被装饰对象的生命周期,谁 new 的谁 delete。这点和代理模式(代理常自己创建目标)不同
装饰器和继承能不能一起用? 可以,而且是常态。装饰器自己就是通过继承 Component 来保证接口一致的------用继承拿类型,用组合加功能
装饰器是线程安全的吗? 装饰器自身的状态(如仓库里的 notdefense、isAdd)如果被多线程共享就不安全;被装饰对象是否安全取决于它自己
多层装饰的性能问题? 每层都是一次虚函数调用 + 一次转发,深层次嵌套会累积开销,同时产生大量小对象
能不能在装饰器里"删掉"原有功能? 语法上可以(不转发就行),但这已经不是装饰模式了。装饰的语义是只增不减
装饰模式和职责链的区别? 职责链的重点是"找谁处理 "------一个人处理完就结束,或传给下一个;装饰器的重点是"层层叠加"------每一层都要执行。两者可以混合,比如 IO 流
装饰模式和策略模式的区别? 策略是换掉整个算法 (一个对象持有多种可互换的实现);装饰是在原算法外面叠加行为(一层层包裹)

6.5 什么时候不该用装饰模式

  • 扩展是固定的、不会变的 → 直接继承或直接写死更简单。装饰模式的价值在于"运行期灵活组合",不需要灵活时它只是徒增间接层;

  • 装饰层数很深、且顺序有严格依赖 → "套几层、什么顺序"本身就变成复杂逻辑,散落在客户端很难维护,应该把这部分封装成一个构建器(Builder)或工厂;

  • 只是想给某个方法加一段逻辑,且只有一处调用 → 直接改代码或抽个函数,别引入模式;

  • 对象数量巨大、对内存敏感 → 装饰模式会产生大量小对象,层层转发也有开销。如果目的是共享对象 ,那应该看享元模式;

  • 需要控制访问、延迟创建 → 那是代理模式的职责,不是装饰器。


结语

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!

相关推荐
蜗牛互联网2 小时前
Gemini 4 Argon的1M输出窗口与长程Agent工程边界
java·人工智能·后端
elseif1232 小时前
【2026 CSP-J】【逐题精讲】超详细(15道选择,3道阅读,2道完善)
java·开发语言·c++·算法·csp
殷色玫瑰3 小时前
C语言编译和链接:从 .c到 .exe,程序到底经历了什么?
linux·c语言·c++·算法
蜗牛互联网3 小时前
从GitHub动态工作流理解确定性编排与Agent判断边界
java·人工智能·后端·github
keep intensify3 小时前
C++20线程池
c++·算法·c++20
汉克老师3 小时前
GESP2026年9月认证C++一级( 第三部分编程题(2、棋盘上的奖赏))精讲
c++·gesp·小学生·学c++编程
努力努力再努力wz3 小时前
【CUDA 入门系列】核函数与线程模型:一文理清 Grid、Block、dim3、内置变量与全局索引
c++·架构
ShineWinsu3 小时前
对于Redis:ZSet类型的解析
数据库·c++·redis·缓存·面试·有序集合·zset
鶴哥只手遮天3 小时前
深入理解OpenSceneGraph(五):插件生态与最佳实践
后端