周三早上,闹钟响的时候,天没亮透,窗户缝里钻进来的不是光,是冷。
小王眼睛都没睁开,伸手把闹钟按掉。被窝里多赖了十秒,还是认命地爬起来,从柜子最底下翻出一条秋裤。
套上秋裤,再穿外裤。下楼,从车筐里抽出挡风被,两根绳子往车把上一系,把车头和腿前那片风整个兜住。
电动车还是去年那辆,人也还是昨天那个人。谁都没动,只是外面各多了一层。

车没换,人没换,各多了一层
天冷了不用换车,加一层就行。
今天聊的就是这件事在代码里的写法:装饰器(Decorator)------不改本体,在外面包一层,一层不够就再包一层。系列第五篇。
昨天那篇留了个尾巴:优惠要"叠一堆",策略管不了。今天到公司正好收这个尾------测试昨晚报了笔错账,先排查,再开发。
01 为每一层都建个子类
先看一个不太聪明的写法。天一冷,电动车的"配置"开始变:今天加挡风被,明天换加热把套,后天可能还要加对护手。每样都想要,用继承写:
java
// 裸车
class ElectricScooter {
void go() { System.out.println("骑电动车,十分钟到地铁站"); }
}
// 加被子
class ScooterWithCover extends ElectricScooter { /* ... */ }
// 加被子又加把套
class ScooterWithCoverAndGrips extends ScooterWithCover { /* ... */ }
// 只想要把套、不要被子呢?
class ScooterWithGripsOnly extends ElectricScooter { /* ... */ }
每种组合,都得焊一个类
三样配件,非空的组合是七种。再来第四样,十五种。类不是写不完,是"层"被长成了"本体"------挡风被明明开春就该拆下来,代码里它却焊死在车上。
人这边也一样。"穿了秋裤的人""穿了秋裤又加保暖袜的人"......为了几层布,给人开了个族谱。
外加的东西一旦写进本体,拆下来就得动本体。
02 包一层,别动本体
装饰器的解法就一句话:本体和外加层实现同一个接口,每一层手里都拿着里面那层。
java
// 一个接口:能赶路就行
interface Commute {
void go();
}
// 本体:电动车,一行不改
class ElectricScooter implements Commute {
public void go() { System.out.println("骑电动车,十分钟到地铁站"); }
}
// 外加层:挡风被
class WindshieldCover implements Commute {
private final Commute inner; // ← 手里握着里面那层
WindshieldCover(Commute inner) { this.inner = inner; }
public void go() {
System.out.println("挡风被把风挡在外面");
inner.go();
}
}
// 再加一层:加热把套
class HeatedGrips implements Commute {
private final Commute inner;
HeatedGrips(Commute inner) { this.inner = inner; }
public void go() {
System.out.println("把套先热起来");
inner.go();
}
}
三层一个接口,外层管加料,内层管赶路
今天出门,组装就这一行:
java
Commute today = new HeatedGrips(new WindshieldCover(new ElectricScooter()));
today.go();
// 把套先热,被子挡风,最后才是车在跑
从外往里包,调用从外往里传
他自己今早也是这么组装的:先秋裤,再外裤,最后外套------只是人这个"接口",不用写代码。
加一层,只加一个类;拆一层,也只删一个类。

层抱着层,本体在中间没动过
这里有个对不上的地方,得先说清楚。现实里这层看得见:穿反了、穿厚了,身体马上有反馈。代码里的层是堆在内存里的引用,包错顺序编译器不吭声,跑起来才出事------上午就出了一件。
03 先排查,那笔账差两块
到公司还没坐稳,测试在群里 @ 他:昨晚那笔订单,120 块的东西,满 100 减 20 再打 9 折,该收 90,实际扣了 88。
差的两块出在顺序。昨晚赶工,叠加写成了"先打折、再满减":120 打 9 折是 108,108 还够着 100,再减 20,88。运营要的是先减再折:(120 − 20) × 0.9,90。
定位不难,难的是这段叠加写在下单流程的正中间。上一篇说过,主干上的判断,碰一次险一次------这次顺手把它抽出来,按今早的思路重写:
java
// 一个接口:给个价就行
interface Price {
BigDecimal value();
}
// 本体:原始金额
class OrderPrice implements Price {
private final BigDecimal amount;
OrderPrice(BigDecimal amount) { this.amount = amount; }
public BigDecimal value() { return amount; }
}
// 满减层
class FullReduction implements Price {
private final Price inner;
FullReduction(Price inner) { this.inner = inner; }
public BigDecimal value() {
BigDecimal v = inner.value();
return v.compareTo(new BigDecimal("100")) >= 0
? v.subtract(new BigDecimal("20")) : v;
}
}
// 会员层
class MemberDiscount implements Price {
private final Price inner;
MemberDiscount(Price inner) { this.inner = inner; }
public BigDecimal value() {
return inner.value().multiply(new BigDecimal("0.9"));
}
}
每条规则一层,各管各的加减
组装处只剩一行,顺序就写在这一行里:
java
// 先减,后折:120 → 100 → 90
Price p = new MemberDiscount(new FullReduction(new OrderPrice(new BigDecimal("120"))));
p.value();
想换成"先折再减"?调一下嵌套,别动任何一层
昨天说的"叠一堆是装饰器的活",今天兑现:每层只管自己那点规则,里面是谁不关心;叠几种、按什么顺序叠,全在组装处。
和策略一样,装饰器也靠"同一个接口"走天下。差别在语义:策略的接口是"选一个",装饰器的接口是"里面还有一个"。

同一笔 120,两种叠法,两个结果
04 JDK 里全是这个
这套"包一层"的写法,JDK 里最经典的例子你八成写过:
java
InputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("a.txt")));
本体读字节,中间层加缓冲,最外层按类型读
FileInputStream 是本体,只管从文件搬字节;BufferedInputStream 包一层,加缓冲;DataInputStream 再包一层,加按类型读。三层共用一个接口,谁也没改谁。
Collections 也是一路:synchronizedList 包一层加线程安全,unmodifiableList 包一层禁掉写。想要什么性质,就往上包什么。
05 什么时候,别乱加层
装饰器的门槛:**有"外加的性质",而且这些性质要自由组合。**如果增强只有一种、十年不变,直接改本体更省事,犯不上为它单开一层。
层也不是越多越好。现实里穿三条秋裤,先迈不开的是腿;代码里套五层,一次调用要在五层之间跳,排查问题得一层层拆。今早那两块钱要是套在五层里,光找顺序就得拆五遍。
顺序敏感,编译器还不管。先穿秋裤再穿外裤,天经地义,没人会搞反;先满减再打折、还是先折再减,编译器不拦你。所以顺序只能写在组装处,别藏进任何一层里。
和策略的分工再说一遍:策略是同一个位置换一个,装饰器是同一个本体叠几层。策略的零件互不认识,装饰器的层一个抱一个。
本体不动,层叠在外。
06 下班,把层裹紧
下班,天黑透了,比早上更冷。
他把挡风被侧面的绳子又勒紧一道,钥匙一拧,汇入非机动车道的灯河。腿前那片风被兜在外面,秋裤顶住剩下那点。
车还是去年那辆车,人还是那个人。谁都没动,只是各多了一层。
这大概是装饰器最像生活的地方:它不改变你是谁,只在你和天气之间,多垫一层。
跟前面四篇对一句:单例管"造几个",工厂管"怎么造",模板方法管"事情怎么走",策略管"这一步用什么做法",装饰器管"在本体外面加几层"。
带走一句话:装饰器 = 同一个接口,外面包一层,本体不拆。
能加一层解决的,别动原来的那一层。
参考资料
- 📕 《设计模式:可复用面向对象软件的基础》(GoF 原著)------装饰器的原始定义与"组件---装饰器"结构
- 📗 《Head First 设计模式》第 3 章------星巴兹咖啡,加料就是加层,本文"秋裤与挡风被"的同乡
- 📘 Java 官方文档 · InputStream
- 📙 Java 官方文档 · Collections(synchronizedList / unmodifiableList)