【设计模式】装饰模式(一)实战:从计费系统的"动态加功能"说起
前面我们聊了装饰模式"怎么用"------套娃式嵌套,动态加功能。能跑通,也写得很优雅。
但今天我们要聊的是:它为什么这么写?
引言:从核心出发
装饰模式的定义只有一句话:动态给一个对象添加额外的职责。
就这一句话,展开来其实有三个核心要求:
-
对调用方透明------加了功能,调用方的代码不能变,还是调原来的方法
-
不改变核心类------加功能的过程中,原来的类一个字都不用改
-
运行时动态扩展------不是编译期写死的,而是运行的时候才决定加什么、怎么组合
这三个要求,每一个都对应一套精心的设计决策:
-
同接口继承 + 向上转型 → 实现透明化
-
聚合关联 + 方法重写委托 → 实现不改变核心类
-
聚合抽象类型 + 多态 → 实现运行时动态扩展
下面我们逐个拆解这三个决策,分析多层嵌套的委托链执行逻辑,讲解装饰模式的取舍代价,最后回归面向对象核心公理做深度升华。
一、透明化:穿了衣服还是小明
1.1 先说清楚"透明"是什么意思
装饰模式的第一条核心要求:对调用方透明。
透明的核心含义:为对象新增功能后,调用方代码无需任何修改,依旧调用原有方法、使用原有父类类型,完全感知不到功能扩展。
通过两段代码直观理解该特性:
java
// 被调用方:负责创建对象,返回给调用方
class PersonFactory {
public Person createDressedPerson() {
Person xiaoMing = new ConcretePerson("小明");
return new Jacket(new Shirt(xiaoMing)); // 返回类型始终是 Person
}
}
// 调用方:只管拿 Person 调用方法,完全不知道内部嵌套层数
class Client {
public void main() {
PersonFactory factory = new PersonFactory();
Person p = factory.createDressedPerson();
p.show(); // 调用方代码全程不变
}
}
核心关键点:调用方获取的对象类型是 Person,调用的是 Person 顶层声明的 show() 方法。
即便后续修改装饰组合逻辑,新增装饰层级:
java
// 被调用方:更换装饰组合,新增领带装饰
public Person createDressedPerson() {
Person xiaoMing = new ConcretePerson("小明");
return new Tie(new Jacket(new Shirt(xiaoMing)));
}
调用方代码零改动 。Client 中的 p.show() 完全无需调整,感知不到内部从「衬衫+夹克」变为「衬衫+夹克+领带」。
这就是透明化的核心:新增功能,不增加调用方任何负担。
1.2 代码如人生:穿了衣服还是小明
小明穿上夹克,本质还是小明,身份、交互方式完全不变,只是新增了穿搭属性。
装饰模式正是如此:无论内部嵌套多少层装饰器,对外暴露的始终是顶层父类接口。调用方无需判断对象具体装饰类型,只需要按照顶层接口规范调用方法即可。
1.3 怎么做到的:同接口继承
透明化的核心前提:装饰器与核心类实现同一套顶层接口/继承同一个抽象类,保证对外身份统一。
java
// 顶层抽象接口
abstract class Person {
public abstract void show();
}
// 核心业务类
class ConcretePerson extends Person { ... }
// 装饰器抽象父类(和核心类同接口)
abstract class Finery extends Person { ... }
// 具体装饰器
class Jacket extends Finery { ... }
class Shirt extends Finery { ... }
ConcretePerson(核心类)和 Finery(装饰抽象类)均继承自 Person,所有具体装饰器都是 Person 的子类。
因此装饰器对象可以通过向上转型 ,赋值给 Person 类型引用,实现对外透明。
1.4 向上转型背后的原则
向上转型的语法支撑来自 Java 语法,但其设计约束来源于里氏代换原则(LSP):
子类对象可以无缝替换父类对象,出现在任何父类可出现的场景中,且行为无异常。
正因所有装饰器、核心类都遵循 LSP,行为与顶层 Person 规范兼容,调用方通过父类引用调用方法时,不会出现任何逻辑异常,透明化设计才得以成立。
1.5 一句话总结
透明化的本质:装饰器伪装成顶层接口类型,对外完全屏蔽内部装饰细节,调用方无感知、无需适配。
二、不改变核心类:给小明披外套
2.1 先说清楚"不改变"是什么意思
装饰模式第二条核心要求:核心业务类代码零修改。
给小明添加穿搭,不需要修改小明本身的属性和行为,衣服是独立个体,人是核心主体,两者完全解耦。
核心类一旦定义完成,永久无需改动,所有功能扩展全部通过新增装饰器实现:
java
// 核心类:写完后永久无需修改
class ConcretePerson extends Person {
private String name;
public ConcretePerson(String name) { this.name = name; }
public void show() {
System.out.println("打扮好的" + name);
}
}
// 抽象装饰器:独立于核心类存在
abstract class Finery extends Person {
protected Person component;
public Finery(Person component) { this.component = component; }
}
// 具体装饰器:衬衫装饰
class Shirt extends Finery {
public Shirt(Person component) { super(component); }
public void show() {
super.show();
System.out.println("穿衬衫");
}
}
// 具体装饰器:夹克装饰
class Jacket extends Finery {
public Jacket(Person component) { super(component); }
public void show() {
super.show();
System.out.println("穿夹克");
}
}
核心关键点:ConcretePerson 完全不依赖任何装饰器类,不知道自己会被装饰、装饰多少次。单向依赖:仅装饰器依赖核心类,核心类对装饰器无感知。
2.2 代码如人生:给小明披外套
穿搭是披在人外部的附属属性,不需要改造人体本身。装饰模式同理,装饰器是独立的功能扩展单元,与核心类无硬耦合,可随意新增、移除、替换,完全不影响核心业务逻辑。
2.3 怎么做到的:聚合关联
核心与装饰器的"松散绑定",依靠聚合关联实现:装饰器内部持有顶层接口类型的对象引用,而非继承核心类。
java
abstract class Finery extends Person {
// 聚合:持有抽象类型对象引用,而非具体核心类
protected Person component;
public Finery(Person component) {
this.component = component;
}
}
多层装饰的嵌套本质,就是逐层聚合绑定:
java
new Jacket(new Shirt(xiaoMing));
// Jacket 聚合 Shirt 对象
// Shirt 聚合 核心类 xiaoMing 对象
每一层装饰器都通过 component 引用指向内层对象,彼此独立、互不改造,仅通过引用完成关联。
2.4 怎么生效的:重写 + 委托转发
聚合仅完成对象关联,真正实现功能装饰的核心是:重写顶层方法 + 请求委托转发。
具体装饰器重写方法,先将请求转发给内层对象,再执行自身的扩展逻辑:
java
class Jacket extends Finery {
public Jacket(Person component) { super(component); }
@Override
public void show() {
super.show(); // 委托:执行内层所有逻辑
System.out.println("穿夹克"); // 自身新增扩展功能
}
}
父类装饰器的 super.show() 实现统一委托逻辑:
java
@Override
public void show() {
if (component != null) {
component.show(); // 将请求转发给内层被包装对象
}
}
完整执行链路:Jacket.show() → Finery.show() → Shirt.show() → 底层ConcretePerson.show()
每一层装饰器的统一逻辑:接收请求 → 委托内层执行 → 追加自身扩展功能。
2.5 一句话总结
不改变核心类的本质:核心类与装饰器完全解耦,通过聚合实现松散绑定,依靠方法重写与委托实现功能扩展,对核心类实现零侵入。
三、运行时动态扩展:今天穿什么出门现决定
3.1 先说清楚"动态"是什么意思
装饰模式第三条核心要求:功能组合在运行期动态决定,编译期不写死。
这是装饰模式相较于继承的核心优势:
-
继承是静态绑定 :定义子类
PersonWithShirt后,编译期就固定绑定功能,运行期无法修改 -
装饰是动态绑定:无需新增类,仅通过调整嵌套方式,即可在运行期自由组合功能
动态扩展示例:
java
Person xiaoMing = new ConcretePerson("小明");
// 场景一:衬衫+夹克
Person dressedA = new Jacket(new Shirt(xiaoMing));
dressedA.show();
// 场景二:衬衫+领带
Person dressedB = new Tie(new Shirt(xiaoMing));
dressedB.show();
// 场景三:三层全装饰
Person dressedC = new Tie(new Jacket(new Shirt(xiaoMing)));
dressedC.show();
同一个核心对象 xiaoMing,运行期可随意切换装饰组合、增减装饰层级,无需修改任何代码、无需新增类。
3.2 代码如人生:出门前看天气决定穿什么
继承相当于天生自带穿搭 ,功能从类定义时就固定,无法更改;装饰模式相当于出门前按需穿搭,根据运行时场景动态组合功能,灵活适配不同业务场景。
继承将功能焊死在类结构中,装饰模式将功能拆分为独立可复用的功能零件,运行期自由拼接。
3.3 怎么做到的:聚合的是抽象类型
动态扩展的核心关键:装饰器聚合的是抽象父类类型,而非具体实现类。
java
// 核心:聚合抽象类型,而非 ConcretePerson
protected Person component;
正因为 component 是 Person 抽象类型,运行期可以绑定任意 Person 子类对象:
java
new Jacket(xiaoMing); // 绑定核心对象
new Jacket(new Shirt(xiaoMing)); // 绑定衬衫装饰对象
new Jacket(new Tie(xiaoMing)); // 绑定领带装饰对象
编译期仅定义规范,运行期 new 对象时才完成真实实现绑定,彻底摆脱编译期固化限制。
对比继承:
-
继承:
class A extends B→ 编译期固定父子关系,永久不可修改 -
装饰:
new Jacket(任意Person子类)→ 运行期动态绑定实现
3.4 两层多态
装饰模式的动态透明能力,依赖两层多态共同实现:
| 层次 | 代码体现 | 核心作用 |
|---|---|---|
| 外层多态 | Person p = new Jacket(...) |
保证调用方透明,对外始终是统一接口 |
| 内层多态 | component.show() 动态执行对应子类方法 |
保证运行期可动态切换内部实现,实现功能扩展 |
外层屏蔽变化、内层实现变化,两者结合实现「对外透明统一,对内灵活扩展」。
3.5 一句话总结
运行时动态扩展的本质:基于抽象类型聚合 + 多态机制,编译期仅定义规范,运行期动态绑定具体实现,实现功能自由组合、灵活扩展。
四、委托链与执行顺序:穿衣的先后
4.1 先说清楚问题
多层嵌套装饰时,方法执行顺序由 super.show() 的位置决定,位置不同,输出顺序完全反转:
标准嵌套代码:
java
Person dressed = new Tie(new Jacket(new Shirt(xiaoMing)));
dressed.show();
默认写法(super在前)输出:
Plain
打扮好的小明
穿衬衫
穿夹克
打领带
若将 super.show() 后置:
java
class Jacket extends Finery {
@Override
public void show() {
System.out.println("穿夹克");
super.show();
}
}
输出完全反转:
Plain
打领带
穿夹克
穿衬衫
打扮好的小明
下面拆解核心原理。
4.2 代码如人生:拆快递盒
多层装饰嵌套如同多层快递包装:外层是领带、中层是夹克、内层是衬衫、核心是小明。
代码执行逻辑:从外层向内层递归调用,从内层向外层依次执行输出,和拆快递的逻辑完全一致。
4.3 链式委托,不是递归
装饰模式的多层调用看似递归,本质是链式委托,两者核心区别如下:
| 对比项 | 递归 | 装饰模式委托 |
|---|---|---|
| 定义 | 方法调用自身 | 当前对象将请求转发给内层其他对象 |
| 调用目标 | 同一个方法、同一个对象 | 不同对象的同名方法 |
| 示例 | factorial(n) 调用自身 |
Jacket.show() 调用 Shirt.show() |
看似自相似的调用栈,本质是多个不同对象的链式转发,而非递归。
4.4 调用栈展开过程
super.show() 在前时的完整执行链路:
-
Tie.show()执行 → 调用Finery.show()→ 转发给Jacket.show() -
Jacket.show()执行 → 调用Finery.show()→ 转发给Shirt.show() -
Shirt.show()执行 → 调用Finery.show()→ 转发给ConcretePerson.show() -
底层核心方法执行完毕,逐层返回,依次执行各层装饰器输出
执行顺序:核心对象 → 内层装饰 → 外层装饰(从内到外)。
4.5 为什么 super 前后顺序影响输出
super.show() 是一次完整的方法调用,会暂停当前方法逻辑,跳转执行内层方法,待内层完全执行完毕后,再继续执行当前方法剩余逻辑。
-
super在前 :先委托内层执行,再执行自身逻辑 → 从内到外输出
-
super在后 :先执行自身逻辑,再委托内层执行 → 从外到内输出
该特性完全遵循栈(LIFO 后进先出)的底层逻辑。
速记口诀:super在前,先人后己(从内到外);super在后,先己后人(从外到内)。
4.6 内存里的引用链
程序运行时,堆内存中的对象引用会形成一条单向链式结构 :

所有装饰器通过 component 引用串联,JVM 调用方法时,沿引用链逐层向下穿透,到底后逐层返回执行扩展逻辑。
4.7 一句话总结
委托链的本质:多层装饰器通过引用链逐层转发请求,super 方法的位置决定执行顺序,依托栈的后进先出特性,实现有序的功能叠加。
五、盈亏同源:装饰模式的代价
5.1 没有免费的午餐
装饰模式的透明化、无侵入、动态扩展三大优势,极大提升了代码灵活性,但所有优势都伴随对应的取舍代价,盈亏同源,无完美设计。
5.2 优势与代价逐项对照
| 核心优势 | 对应设计代价 |
|---|---|
| 运行时动态组合功能 | 嵌套代码可读性差,多层嵌套逻辑不直观,排查问题需逐层拆解 |
| 避免继承导致的类爆炸 | 产生大量碎片化小对象,多层装饰造成堆内存对象冗余 |
| 遵循开闭原则(新增不改旧) | 删除、调整装饰层级顺序,必须修改客户端创建代码 |
| 接口统一、调用透明 | 装饰器新增特有方法时,会破坏透明性,客户端需强制转型才能调用(半透明装饰)(比如 Jacket 想加个 takeOff()),透明性就破了------客户端必须强转才知道调 takeOff() |
| 支持任意层级灵活嵌套 | 嵌套顺序影响结果,而且没有编译期检查保证顺序正确。穿了五层之后,调用链像洋葱,出 bug 得一层层剥 |
5.3 代码如人生:灵活的代价
穿搭越灵活,需要决策的组合就越多,层级越多越容易混乱。装饰模式同理,极致的动态灵活性,换来的是代码隐蔽性增强、调试难度提升、调用链路不直观的问题。
小明今天可以穿衬衫+夹克,明天可以穿夹克+衬衫,后天可以只穿一件T恤。灵活吧?但代价是------他每天出门前得想"今天穿什么组合",衣柜里衣服越多选择越难。而且穿了五层之后,他自己都忘了最里面穿了什么。
这不是小明的问题,是所有追求灵活性的方案共同的宿命。你选了"极致灵活",碎片化就是你的门票------没有哪个方案能让你既极致灵活又零代价。
5.4 真实痛点:Java I/O
Java I/O 框架是装饰模式的经典应用,也是最典型的代价体现:
java
InputStream in = new BufferedInputStream(
new DataInputStream(
new FileInputStream("data.txt")
)
);
优势极致灵活:按需叠加缓冲、数据解析、文件读取等功能;
代价极其明显:嵌套层级深、对象数量多、调试链路长、资源关闭繁琐、新人上手难度大。
这不是框架实现缺陷,是装饰模式固有取舍。
5.5 一句话总结
装饰模式的取舍本质:用编译期的简洁稳定,换取运行期的极致灵活;以对象碎片化、链路隐蔽性为代价,解决继承固化僵化的问题。无银弹,只有场景适配。
六、回到公理:没有新东西
6.1 走完一圈,回头看
拆解完整套装饰模式的设计逻辑后,我们发现:装饰模式没有创造任何新的语法、新的机制,所有能力都来源于面向对象的基础公理。
6.2 用到的公理/原则一览
| 面向对象公理/原则 | 在装饰模式中的核心作用 |
|---|---|
| 里氏代换原则(LSP) | 保证装饰器子类可无缝替换核心类,实现调用透明 |
| 向上转型 | 实现子类对象向父类引用赋值,统一对外接口类型 |
| 多态机制 | 外层保证透明、内层保证动态扩展,实现双层灵活能力 |
| 组合(聚合)优于继承 | 通过对象聚合替代类继承,实现无侵入功能扩展 |
| 开闭原则 | 新增装饰器扩展功能,不修改核心原有代码 |
| 单一职责原则 | 每个装饰器仅负责一项独立功能,核心类仅负责核心业务 |
6.3 回头看逻辑链条
装饰模式的所有设计,从面向对象的公理出发,在 Java 的语法约束下,每一步都是近乎必然的推导:
- 要调用透明 → 只能同接口继承 + 向上转型(Java 里没有别的语法能让不同类型对调用方透明)
- 要不改核心类 → 只能从外部聚合 + 委托(继承要加功能要么改父类源码、要么被 final 拦住,不碰核心类源码就只剩聚合这一条路)
- 要运行时动态 → 只能聚合抽象类型 + 多态(编译期绑定做不到运行期才决定)
- 要多层扩展 → 自然形成链式委托 + 栈顺序执行(这是 JVM 调用栈的物理结果,不是人为设计)
- 要极致灵活 → 必然付出碎片化、隐蔽性的代价(trade-off 无例外------你选了灵活,碎片化就是门票,没有免费午餐)
6.4 升华
设计模式从不是黑魔法,只是面向对象基础公理的最优组合实践。
装饰模式没有创造新能力,只是将多态、组合、LSP、开闭原则等基础规则,按照工程问题的需求,有序组合、落地落地,解决了「对象动态无侵入扩展功能」的经典问题。
真正掌握装饰模式,不是背诵代码模板,而是建立设计思维:
-
能用组合就不用继承,解耦固化依赖
-
能用抽象统一接口,就对外屏蔽实现细节
-
能把变化抽离为独立单元,就不污染核心逻辑
公理 + 逻辑 = 设计模式。所有优雅的设计,都是基础规则的合理运用。
下篇预告
第一篇:上手使用,掌握装饰模式写法;
第二篇:底层原理,吃透多态、LSP、组合核心逻辑;
下一篇:框架源码实战,深度解析 Java I/O、Spring HttpServletRequestWrapper、MyBatis 缓存装饰器,看大厂源码如何极致运用装饰模式。敬请期待!