CS-Notes 设计模式精讲:装饰(Decorator)模式——以组合动态扩展对象功能的入门到源码佐证
装饰(Decorator)模式是 CS-Notes 设计模式系列(notes/设计模式 - 装饰.md)中结构型模式的核心成员,它允许在不修改现有类代码的前提下,将对象逐层"包裹",从而动态地为对象添加职责与功能。本文以经典的"饮料加配料计费"案例为主线,完整继承原文档的类图、代码与设计原则,并对照 JDK 中 java.io、java.util.zip、java.util.Collections 等真实实现,帮你彻底掌握装饰模式"组合优于继承"的精髓,以及它在实际代码库中(如 Java I/O 体系)的落地方式。
一、模式意图(Intent):为对象动态添加功能
装饰模式的核心意图只有一句话:
为对象动态添加功能。
它强调两个关键点:
- 动态:功能是否添加、添加哪一层、按什么顺序添加,都在运行时由调用方决定,而不是在编译期用继承写死;
- 针对对象:装饰发生在"对象"层面,同一个类的不同实例可以拥有完全不同的功能组合,互不影响。
对比来看,传统继承是"静态的、类层面的扩展":一旦使用继承,所有子类实例都会固定获得父类的能力,并且每增加一种能力组合都可能需要新增一个子类,导致类数量爆炸。装饰模式则把扩展点从"类"下沉到"对象",用组合 + 递归调用实现同样甚至更强的扩展能力。这正是本仓库设计模式笔记在 notes/设计模式.md 概述中所强调的"经验复用"思想的典型应用。
二、类图解析:装饰者与被装饰者同源同构
装饰模式的结构可以用下面这张类图概括(该图同时收录在 notes/设计模式 - 装饰.md 与图片资源目录 notes/pics/6b833bc2-517a-4270-8a5e-0a5f6df8cd96.png):
类图中包含四类角色,它们在"饮料加配料"案例中一一对应:
| 角色 | 抽象层级 | 职责 | 案例对应 |
|---|---|---|---|
| Component(组件) | 抽象 | 定义业务方法的统一接口 | Beverage(含 cost()) |
| ConcreteComponent(具体组件) | 实现 | 真正的基础实现,方法不依赖其它对象,是整个装饰层次的最底层 | DarkRoast、HouseBlend |
| Decorator(装饰者) | 抽象 | 实现组件接口,同时组合一个组件引用,作为所有具体装饰者的基类 | CondimentDecorator |
| ConcreteDecorator(具体装饰者) | 实现 | 在调用被装饰者方法的基础上叠加自己的功能 | Milk、Mocha、Whip |
原文档对这张图的解读值得反复品味:
装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。
从源码结构可以归纳出两个必须记住的要点:
- "继承"保证类型统一:装饰者与具体组件实现同一个
Component接口,因此装饰者本身又可以当作组件被另一层装饰者包裹,形成"装饰器套装饰器"的递归结构; - "组合"保证功能叠加:每个装饰者内部持有
Component引用,调用被装饰者方法后追加自身逻辑——具体组件是装饰层次的最低层,因为只有它的方法实现完全不依赖其它对象。
三、动态装饰的运作模型:逐层包裹与递归调用
理解了类图,再来看运行时的形态。原文档用 "DarkRoast 上加 Mocha、再叠 Whip" 的示意图说明了这一点(原图位于 notes/pics/c9cfd600-bc91-4f3a-9f99-b42f88a5bb24.jpg):
其本质是一个洋葱式的多层包裹结构:
- 最内层是
DarkRoast具体组件,直接返回基础价格; - 中间层
Mocha装饰者包住DarkRoast,其cost()先算出被包裹者的价格,再加上自己代表的价格; - 最外层
Whip装饰者再包住Mocha,同样执行"自己的价格 + 内层价格"。
因此当最外层调用 cost() 时,会发生自外向内、逐层委托的递归调用链:
Whip.cost()
└─> Mocha.cost()
└─> DarkRoast.cost() // 最底层,返回基础价 1.0
每一层装饰者只关心"自己这一层要加什么",并且通过 beverage.cost() 把请求继续向内传递。这样:
- 添加新配料 = 新增一个装饰者类并套上去,不必改动任何已有类;
- 任意多种配料可以任意组合、任意排序,组合数是指数级扩展的,而类的数量只需线性增加。
这正是"装饰"一词的由来——把装饰者像套壳一样套在被装饰者之上。
四、可运行的完整代码实现
原文档给出了一个可编译可运行的完整例子:设计不同种类的饮料,饮料可以动态添加牛奶、摩卡等配料,每增加一种配料价格就累加,最终计算一杯饮料的总价。下面按角色逐一给出代码并补充说明。
4.1 组件接口 Component
public interface Beverage {
double cost();
}
Beverage 是组件接口,它只声明一个 cost() 方法用于返回价格。所有具体组件和装饰者都实现该接口,从而保证"同源同构、可相互嵌套"。
4.2 具体组件 ConcreteComponent
public class DarkRoast implements Beverage {
@Override
public double cost() {
return 1;
}
}
public class HouseBlend implements Beverage {
@Override
public double cost() {
return 1;
}
}
DarkRoast 与 HouseBlend 是两种基础咖啡(具体组件)。它们的 cost() 直接返回一个固定价格(示例中为 1),不依赖任何其它对象——这也决定了它们只能作为装饰层次中的最内层(底层)存在。
4.3 抽象装饰者 Decorator
public abstract class CondimentDecorator implements Beverage {
protected Beverage beverage;
}
CondimentDecorator 是抽象装饰者,它的关键动作是组合一个 Beverage 类型的引用 protected Beverage beverage;。这个引用在运行时指向被它包裹的对象(可能是具体组件,也可能是另一层装饰者)。之所以定义为 protected,是为了让子类(具体装饰者)能够在构造时接收并保存被装饰对象。
抽象装饰者自身不实现 cost(),具体加价逻辑由各个具体装饰者自行决定。
4.4 具体装饰者 ConcreteDecorator
public class Milk extends CondimentDecorator {
public Milk(Beverage beverage) {
this.beverage = beverage;
}
@Override
public double cost() {
return 1 + beverage.cost();
}
}
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public double cost() {
return 1 + beverage.cost();
}
}
以 Milk 为例,它的工作分两步:
- 构造器接收被装饰对象:
public Milk(Beverage beverage)把外层传入的对象保存到继承来的beverage字段中,完成"包裹"动作; cost()先委托后叠加:return 1 + beverage.cost();中的1是牛奶自身加价(示例价格),beverage.cost()递归求出内层对象的价格,两者相加即为整杯饮料价格。
Mocha(摩卡)的结构与 Milk 完全一致。同理,若再加入 Whip(奶泡)、Soy(豆奶)等新配料,只需照此模式新增装饰者类即可,对已有代码零侵入。这也说明:每种具体装饰者"自己的那部分功能"(这里是自身加价)由自己实现,而"被装饰者的功能"通过委托调用保留了下来。
4.5 客户端组装
public class Client {
public static void main(String[] args) {
Beverage beverage = new HouseBlend();
beverage = new Mocha(beverage);
beverage = new Milk(beverage);
System.out.println(beverage.cost());
}
}
3.0
客户端展示了装饰模式的经典用法——多步包裹式赋值:
- 先创建最底层组件
HouseBlend(基础价1.0); - 用
Mocha包一层(+1.0,累计2.0); - 再用
Milk包一层(+1.0,累计3.0)。
最终输出 3.0。全程变量 beverage 的静态类型始终是 Beverage 接口,但运行时对象却从"一杯美式"逐步演化为"一杯加了摩卡和牛奶的美式"——功能在运行时被动态叠加,而客户端代码无需感知任何内部细节,这正是装饰模式对调用方透明性的体现。
如果希望更贴近真实场景,还可以把所有配料价格做成可配置的(例如把魔法数字 1 提取为构造参数或常量),让 cost() 中的自身加价部分也随实例变化——装饰模式的框架不会因此有任何改动。
五、模式背后的设计原则:开闭原则
装饰模式之所以值得学习,不仅仅因为它的结构精巧,更因为它践行了面向对象设计中最重要的原则之一。原文档明确指出:
类应该对扩展开放,对修改关闭:也就是添加新功能时不需要修改代码。饮料可以动态添加新的配料,而不需要去修改饮料的代码。
把这句话落实到"饮料加配料"案例中就是:
- 对扩展开放:想要新增一种配料(如奶泡
Whip),只需新写一个CondimentDecorator子类,然后在客户端"套一层"即可; - 对修改关闭:
Beverage、DarkRoast、HouseBlend以及已存在的所有装饰者类一律不需要改动。
同时,原文档也提醒了一个重要的务实观点:
不可能把所有的类设计成都满足这一原则,应当把该原则应用于最有可能发生改变的地方。
也就是说,开闭原则不是"银弹",不应盲目地为每个类都套上装饰层;它应当被用在需求最可能变化、扩展最频繁的边界上(比如这里的配料种类),否则反而会引入不必要的复杂度。
5.1 装饰模式与继承、类爆炸
通过本案例可以直观体会到装饰模式相比"为每种组合写一个子类"的优势。若采用纯继承方案,一杯咖啡加牛奶、摩卡、奶泡……有多少种配料组合,就要定义多少个"固定配方类"(如 HouseBlendWithMochaAndMilk),类数量随配料种类呈组合爆炸式增长,维护成本极高。而装饰模式将"配方组合"从类的定义中解放出来,交由运行时自由组装,只保留少数几个装饰者类即可覆盖全部组合。这也是该模式在现代框架与类库中被广泛采用的根本原因。
六、JDK 中的装饰模式实例
原文档在末尾给出了装饰模式在 JDK 中的典型落地清单,这些是面试与源码阅读中最高频的例子:
java.io.BufferedInputStream(InputStream)java.io.DataInputStream(InputStream)java.io.BufferedOutputStream(OutputStream)java.util.zip.ZipOutputStream(OutputStream)java.util.Collections#checked[List|Map|Set|SortedSet|SortedMap]()
下面展开说明其中最具代表性的两组,帮助你把抽象的模式映射到真实的 JDK 源码结构上。
6.1 Java I/O:装饰模式最经典的应用
java.io 包整体就是按照装饰模式组织的。以字节输入流 InputStream 为例,本仓库的 notes/Java IO.md("装饰者模式"小节)对此有专门梳理:
FileInputStream等属于具体组件,提供最基础的从文件等数据源读取字节的能力;FilterInputStream属于抽象装饰者,它持有被包装的InputStream引用,是所有装饰流的共同父类;BufferedInputStream、DataInputStream等属于具体装饰者,在转发底层读取请求的同时附加各自的功能。
例如,要为文件流加上缓冲功能,只需要"套一层"装饰者即可:
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
BufferedInputStream为FileInputStream提供缓存能力,减少磁盘 I/O 次数;DataInputStream装饰者则提供了对int、double等更多数据类型的读取能力。
在 notes/Java IO.md 中还可以看到 Reader 一侧的同样组织方式——BufferedReader 通过组合 Reader 对象来叠加缓冲读取能力。这种"外层加壳"的写法与本文饮料案例中 new Milk(new Mocha(new HouseBlend())) 的组装方式完全同构,是验证装饰模式价值的最佳现实教材。
6.2 集合包装视图:Collections.checked*
java.util.Collections 提供的 checkedList、checkedMap、checkedSet、checkedSortedSet、checkedSortedMap 等静态方法,会返回一个包装了原集合的"类型安全视图"。这个包装集合将原集合"包裹"在内部,在每次写入元素时进行类型校验,一旦发现类型不匹配立即抛出 ClassCastException,从而在调试期尽早暴露泛型擦除带来的隐患。它同样符合"装饰者实现组件接口 + 组合原始组件 + 追加校验功能"的结构特征。
顺带一提,Collections 中的 synchronizedList、unmodifiableList 等视图本质上也是同一类"包装—转发—附加功能"思想的不同体现,只是有些实现以静态包装器(相当于简单工厂)的形式对外暴露。
七、应用价值与使用注意
7.1 适用场景
综合原文档的意图、案例与 JDK 实例,装饰模式尤其适合以下场景:
- 需要透明且动态地给单个对象添加职责,且不希望影响同类的其它对象;
- 需要撤销某类扩展(不套该层装饰者即可);
- 当采用继承会产生大量子类、导致"类爆炸"时,用组合替代继承;
- 需要以可叠加、可排序的方式组合多种扩展能力(如 I/O 流的层层过滤、中间件管道)。
7.2 需要注意的代价
- 对象数量增多、结构变深:每一层装饰都是一个对象,层层包裹会带来一定内存开销,且调试时调用栈较深、排错成本上升(这也是 Java I/O 流被诟病"冗长"的原因之一);
- 顺序敏感性:部分装饰者对调用顺序敏感(例如先压缩还是先加密),需要在使用层面约定清楚,模式本身不保证任意顺序都合理;
- 类型识别的困难:如果代码依赖具体类型(而非接口)做判断,装饰后对象的真实类型会"失真",这一点在使用装饰模式设计时要尽量避免。
八、小结
装饰模式用"继承保类型、组合加功能"的巧妙结构,实现了对单个对象运行时、动态、可叠加的功能扩展,并天然契合开闭原则。通过 CS-Notes 中"饮料 + 配料"的完整案例,你可以一次性掌握它的类图角色、递归调用模型与可运行代码;而 java.io、java.util.Collections 等 JDK 实现则证明了这一模式在工业级代码中的真实价值。建议结合本仓库的 设计模式目录 与 Java IO 装饰者小节 进行横向复习,把"模式词汇"沉淀为自己的设计直觉。
本文依据 notes/设计模式 - 装饰.md 编写,完整模式内容与配套代码亦汇总于 notes/设计模式.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

