上一篇代理模式结尾,我们埋了个钩子:代理和装饰器模式(Decorator) 代码结构几乎一样,但意图相反------代理管"控制",装饰器管"增强"。这一篇就来把装饰器讲透,顺便从另一头把这条界限夯实。
装饰器要解决的问题,一句话:在不修改原对象、也不靠继承的前提下,动态地给对象叠加新功能。 关键词是"动态"和"叠加"------你可以在运行时,像穿衣服一样,给一个对象一层层地包上新能力,想加哪层加哪层,顺序随意,还能自由组合。它最形象的比喻就是洋葱:核心是那个原始对象,外面一层层包裹着各种装饰,每一层都在前一层的基础上增强一点点。
这一篇我们用"订单价格计算"这个再典型不过的场景切入:一个订单的最终价格,可能要在原价基础上叠加会员折扣、再叠加满减、再叠加优惠券......而且不同订单叠加的组合千变万化。你会看到,如果用继承来应对这种"任意组合",会立刻陷入类爆炸的泥潭,而装饰器用"组合"优雅地化解了它------这正是第一篇"合成复用原则(组合优于继承)"最好的一个活教材。最后我们还会看 Java IO 流这个装饰器的教科书级应用,并把装饰器和代理彻底辨析清楚。
这篇文章按这条线索展开:先看"价格叠加"用继承会怎样类爆炸;再引出装饰器,用洋葱式包裹优雅解决;然后讲清它的四个角色和骨架;接着剖析它为什么是"组合优于继承"的最佳范例;再看 Java IO 这个最经典的装饰器实例;最后从另一头辨析装饰器与代理的区别,给出适用边界。贯穿例是订单价格的优惠叠加。
目录
- 一个"价格叠加"引发的类爆炸
- 装饰器模式:像洋葱一样层层包裹
- 四个角色与骨架
- 为什么装饰器是"组合优于继承"的活教材
- [Java IO:装饰器的教科书级应用](#Java IO:装饰器的教科书级应用)
- [装饰器 vs 代理:从另一头把界限夯实](#装饰器 vs 代理:从另一头把界限夯实)
- 什么时候用装饰器
一、一个"价格叠加"引发的类爆炸
我们的订单要算最终价格。基础是订单原价,但业务上有一堆可叠加的优惠:
- 会员折扣(会员打 9 折)
- 满减(满 100 减 20)
- 优惠券(立减 10 元)
麻烦在于,这些优惠是可以任意组合的:有的订单只用会员折扣,有的会员折扣 + 满减,有的三个全叠。如果用继承来表达这些组合,你得为每一种组合写一个子类:
java
class Order { double getPrice() { return 100; } }
class MemberOrder extends Order { ... } // 只会员折扣
class FullReductionOrder extends Order { ... } // 只满减
class CouponOrder extends Order { ... } // 只优惠券
class MemberFullReductionOrder extends Order { ... } // 会员 + 满减
class MemberCouponOrder extends Order { ... } // 会员 + 优惠券
class FullReductionCouponOrder extends Order { ... } // 满减 + 优惠券
class MemberFullReductionCouponOrder extends Order { ... } // 三个全叠
// ... 3 种优惠就已经 7 个组合类了
看出灾难了吗?n 种优惠,组合起来就是 2ⁿ − 1 个子类。 3 种就 7 个,再加一个"限时折扣",立刻变成 15 个,以后每加一种优惠,类的数量翻倍式增长。这就是继承在"多维度自由组合"面前的死穴------继承是静态的、编译期定死的,它没法表达"运行时任意搭配"这件事。这正是第一篇讲合成复用原则时预告过的"类爆炸"。
问题的本质是:每种优惠其实是一个独立的、可插拔的增强 ,它们本不该被继承关系锁死成一个个固定组合。我们需要的是------把每种优惠做成一个独立的"装饰",谁想要哪几种,就在运行时把它们一层层套上去。 这就是装饰器。
二、装饰器模式:像洋葱一样层层包裹
装饰器的核心思路:让"装饰"和"被装饰的对象"实现同一个接口,装饰持有一个被装饰对象的引用,在调用它的方法时,先(或后)加上自己的增强逻辑,再委托给内部对象。 因为装饰本身也实现了同一接口,所以它又可以被下一层装饰继续包裹------层层嵌套,就像洋葱。
先定义共同接口和原始对象:
java
public interface Order {
double getPrice(); // 算价格
}
// 原始对象:基础订单
public class BasicOrder implements Order {
private final double price;
public BasicOrder(double price) { this.price = price; }
public double getPrice() { return price; }
}
关键是一个抽象装饰器,它也实现 Order,并持有一个 Order:
java
// 抽象装饰器:也是一个 Order,同时"包着"一个 Order
public abstract class OrderDecorator implements Order {
protected final Order order; // 被装饰的对象
public OrderDecorator(Order order) { this.order = order; }
}
然后每种优惠是一个具体装饰器,各自只管自己那一点增强:
java
// 会员折扣:在内部价格上打 9 折
public class MemberDecorator extends OrderDecorator {
public MemberDecorator(Order order) { super(order); }
public double getPrice() {
return order.getPrice() * 0.9; // 先拿内部价,再叠加自己的逻辑
}
}
// 满减:满 100 减 20
public class FullReductionDecorator extends OrderDecorator {
public FullReductionDecorator(Order order) { super(order); }
public double getPrice() {
double p = order.getPrice();
return p >= 100 ? p - 20 : p;
}
}
// 优惠券:立减 10
public class CouponDecorator extends OrderDecorator {
public CouponDecorator(Order order) { super(order); }
public double getPrice() {
return order.getPrice() - 10;
}
}
用起来,就是把想要的优惠一层层套上去,像穿衣服:
java
Order order = new BasicOrder(100); // 原价 100
order = new MemberDecorator(order); // 套上会员折扣 → 90
order = new FullReductionDecorator(order); // 再套满减 → 90(不满100不减)
order = new CouponDecorator(order); // 再套优惠券 → 80
System.out.println(order.getPrice()); // 80
对比第一节的类爆炸,升级点非常清晰:3 种优惠,现在只需要 3 个装饰器类,而不是 7 个组合类;n 种优惠是 n 个类,而不是 2ⁿ 个。 而且组合是运行时 决定的------想怎么叠就怎么叠,想换顺序就换顺序,全靠 new 的时候自由搭配,一个新组合都不用新增类。加一种"限时折扣"?加一个 LimitTimeDecorator 就行,已有代码一律不动。这就是装饰器的威力。
三、四个角色与骨架
把上面的代码抽象一下,装饰器模式有四个角色:
| 角色 | 本例中是谁 | 职责 |
|---|---|---|
| 抽象构件(Component) | Order 接口 |
定义对象和装饰共同的接口 |
| 具体构件(ConcreteComponent) | BasicOrder |
被装饰的原始对象、核心 |
| 抽象装饰器(Decorator) | OrderDecorator |
实现 Component,持有一个 Component 引用 |
| 具体装饰器(ConcreteDecorator) | MemberDecorator 等 |
叠加具体的增强逻辑 |
最关键的一点在抽象装饰器上:它既 implements Component(所以它自己也是一个 Component,能被当作 Order 使用、也能被继续包裹),又持有一个 Component 引用(所以它能包住别人)。 正是这"既是又持有"的双重身份,让装饰器能够无限层层嵌套。用一张洋葱图看最直观:

这张图揭示了装饰器调用的精髓------调用像穿过洋葱一样"由外向内"层层深入,结果再"由内向外"层层加工返回 。当你调用最外层 CouponDecorator.getPrice() 时:它要先知道内层价格,于是调 FullReductionDecorator.getPrice(),后者又调 MemberDecorator.getPrice(),再调到最核心的 BasicOrder.getPrice() 返回 100;然后 100 一层层往外走:会员折扣加工成 90、满减看不满 100 不动还是 90、优惠券减 10 变 80。每一层只关心"在内层结果上加自己这一点",完全不知道整条链有多长、别的层是谁------这就是低耦合。
四、为什么装饰器是"组合优于继承"的活教材
第一篇讲合成复用原则时说过一句话:"很多你以为需要继承的场景,其实用组合更好。"装饰器就是这句话最完美的证明。我们把两种方案正面对一下账。
继承方案(第一节)的问题:
- 类爆炸:每个组合一个子类,2ⁿ 级增长;
- 静态、编译期定死 :一个订单一旦
new成MemberOrder,它就永远是会员订单,运行时没法再给它加满减; - 组合关系写死在类里:想调整叠加顺序、动态增减优惠,统统做不到。
装饰器方案(组合)的优势,正好逐条对上:
- 类的数量是线性的:n 种增强 = n 个装饰器,不随组合数膨胀;
- 运行时动态组合:想叠哪几层、什么顺序,都在运行时用代码决定,极其灵活;
- 符合开闭原则:加新增强 = 加新装饰器,不碰任何已有代码;
- 每个装饰器职责单一:一个只管会员折扣、一个只管满减,清清爽爽(单一职责)。
这里有个值得琢磨的细节:上面的 OrderDecorator 用了 extends(继承)。这不矛盾吗,不是说组合优于继承吗? 关键要看继承用在哪:装饰器用继承,只是为了让各个具体装饰器复用"持有一个 Order 引用"这个共同结构(一种代码复用),而它实现"叠加功能"这件核心的事,靠的是内部那个 order 引用------也就是组合 。换句话说,装饰器的骨架是"组合"(持有并委托),继承只是个用来共享字段的辅助。真正让功能能层层叠加的,是组合,不是继承。这也提醒我们:"组合优于继承"不是"禁用继承",而是"用组合承担核心的能力扩展,继承退居为纯粹的代码复用"。
五、Java IO:装饰器的教科书级应用
如果你觉得订单例子还不够有说服力,那 Java 标准库里有一个所有人天天用、却未必意识到是装饰器的经典------java.io 包。你一定写过这样的代码:
java
// 一层层包起来,是不是和订单优惠叠加一模一样?
InputStream in = new BufferedInputStream( // 第三层:加缓冲
new GZIPInputStream( // 第二层:加解压
new FileInputStream("data.gz"))); // 核心:从文件读
这就是标准的装饰器!对照一下角色:
- 抽象构件 :
InputStream(所有流的共同抽象); - 具体构件 :
FileInputStream(真正从文件读字节的核心); - 抽象装饰器 :
FilterInputStream(持有一个InputStream); - 具体装饰器 :
BufferedInputStream(加缓冲能力)、GZIPInputStream(加解压能力)、DataInputStream(加读取基本类型的能力)......
Java IO 为什么要这么设计?因为"数据来源"(文件、网络、内存、数组)和"处理能力"(缓冲、加密、压缩、编码转换)是两个可以任意组合的维度 。如果用继承,就会出现 BufferedFileInputStream、GzipBufferedFileInputStream、BufferedNetworkInputStream......又一次 2ⁿ 类爆炸。而装饰器让你像搭积木一样自由组合 :任何数据源,想加缓冲就包一层 BufferedInputStream,想加解压就包一层 GZIPInputStream,顺序、组合完全自由。理解了装饰器,你才算真正读懂了 java.io 那套"套娃"式的 API 为什么长这样。
六、装饰器 vs 代理:从另一头把界限夯实
上一篇从代理那头说过它俩"结构相同、意图相反",这一篇我们从装饰器这头再夯一遍,让这条界限彻底清晰。
先承认它们的共同点:都实现同一个接口、都持有一个同接口类型的对象、都在方法调用前后插入逻辑再委托给内部对象。光看代码,一个装饰器和一个代理确实可能长得几乎一样。区别在意图和用法上:
| 代理 Proxy | 装饰器 Decorator | |
|---|---|---|
| 意图 | 控制访问(能不能访问、怎么访问) | 增强功能(叠加新能力) |
| 对内部对象的态度 | 代理自己创建/管理真实对象,调用方常不知其存在 | 被装饰对象从外部传入,调用方明确知道自己在包装 |
| 数量 | 通常一层(一个代理管一个真实对象) | 通常多层(为了叠加,层层嵌套是常态) |
| 关注点 | 真实对象之外的事(日志、权限、事务、远程) | 真实对象本身能力的增强(缓冲、折扣、压缩) |
| 典型场景 | Spring AOP、RPC、权限校验 | Java IO、优惠叠加 |
最好记的一条判据是这个:看"内部对象是谁给的"。
- 装饰器 :内部对象是你从外面
new好、主动传进去 的(new MemberDecorator(order))------你清楚地知道自己在一层层包装,目的是给它加料。"层层叠加"是装饰器的常态。 - 代理 :内部对象通常是代理自己内部持有/创建、你甚至看不到 的(Spring 帮你生成代理,真实对象藏在里面)------你要的是"访问它"这件事被管控,而不是给它加料。"只有一层、替你把关"是代理的常态。
一句话收束:你主动一层层往上包,是装饰器;它替你挡在前面把关,是代理。 结构骗不了你,意图才是分水岭。
七、什么时候用装饰器
老规矩,泼冷水。装饰器很优雅,但它有代价:层层嵌套会产生很多小对象,而且调试时调用栈会变得很深 ------一个 getPrice() 要穿过好几层装饰器,排查问题时得一层层跟进去。所以它也不是万能的。
适合用装饰器的信号:
- 你需要给对象动态地、可选地、可叠加地 添加功能,且这些功能能自由组合;
- 用继承会导致类爆炸(多个独立维度的组合);
- 希望增强逻辑能在运行时灵活搭配,而不是编译期写死。
不必用装饰器的信号:
- 功能组合是固定的、就那么一两种------那直接写死或用继承反而更简单,套装饰器是杀鸡用牛刀;
- 你要的其实是"控制访问"而非"增强能力"------那是代理的活;
- 增强只有一种、也不需要叠加------直接改或包一层普通方法就行。
判断的核心还是那句贯穿全系列的话:先确认真的存在"多个可自由组合的增强维度"这个事实,装饰器才配得上它的嵌套复杂度。 如果优惠就固定一种,硬套一套 Component/Decorator 的四角色结构,就是典型的过度设计。
小结 。装饰器模式让你在不改原对象、不靠继承的前提下,像洋葱一样给对象动态、可叠加地 包裹新功能。它的核心是"既是 Component 又持有 Component "的双重身份,靠组合(持有并委托)实现层层嵌套,靠这一点优雅地干掉了继承在"自由组合"面前的 2ⁿ 类爆炸------是"组合优于继承"最好的活教材。Java IO 那套 new BufferedInputStream(new GZIPInputStream(...)) 的套娃 API,就是它最经典的应用。记住它和代理的分水岭:你主动层层包裹去增强,是装饰器;它替你挡在前面做管控,是代理。 下一篇我们讲适配器模式------当你手上的接口和你想用的接口对不上时(比如对接一个第三方物流 API),适配器就是那个"转接头"。