【设计模式】装饰模式(三):从装饰模式看 Java 与面向对象设计(原理篇)

【设计模式】装饰模式(一)实战:从计费系统的"动态加功能"说起

【设计模式】装饰模式(二):认识装饰模式

前面我们聊了装饰模式"怎么用"------套娃式嵌套,动态加功能。能跑通,也写得很优雅。

但今天我们要聊的是:它为什么这么写?

引言:从核心出发

装饰模式的定义只有一句话:动态给一个对象添加额外的职责。

就这一句话,展开来其实有三个核心要求:

  • 对调用方透明------加了功能,调用方的代码不能变,还是调原来的方法

  • 不改变核心类------加功能的过程中,原来的类一个字都不用改

  • 运行时动态扩展------不是编译期写死的,而是运行的时候才决定加什么、怎么组合

这三个要求,每一个都对应一套精心的设计决策:

  • 同接口继承 + 向上转型​ → 实现透明化

  • 聚合关联 + 方法重写委托​ → 实现不改变核心类

  • 聚合抽象类型 + 多态​ → 实现运行时动态扩展

下面我们逐个拆解这三个决策,分析多层嵌套的委托链执行逻辑,讲解装饰模式的取舍代价,最后回归面向对象核心公理做深度升华。

一、透明化:穿了衣服还是小明

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;

正因为 componentPerson 抽象类型,运行期可以绑定任意 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() 在前时的完整执行链路:

  1. Tie.show() 执行 → 调用 Finery.show() → 转发给 Jacket.show()

  2. Jacket.show() 执行 → 调用 Finery.show() → 转发给 Shirt.show()

  3. Shirt.show() 执行 → 调用 Finery.show() → 转发给 ConcretePerson.show()

  4. 底层核心方法执行完毕,逐层返回,依次执行各层装饰器输出

执行顺序:核心对象 → 内层装饰 → 外层装饰(从内到外)。

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 缓存装饰器,看大厂源码如何极致运用装饰模式。敬请期待!

相关推荐
Zane19944 小时前
从一次支付渠道扩展需求,看懂"多用组合少用继承"到底在说什么
设计模式
geovindu5 小时前
CSharp:Condition Variable Pattern
后端·设计模式·c#·.net·.netcore·条件变量模式·同步型模式
geovindu5 小时前
sql: Data Modeling Patterns using mysql
sql·mysql·设计模式·数据库开发
sarasuki1 天前
失败重试:Agent 中指数退避的正确姿势
人工智能·设计模式·agent
Zane19941 天前
为什么你写的 Java 代码"看着面向对象、实际是面向过程"?
设计模式
geovindu1 天前
sql: Data Modeling Patterns
数据库·sql·设计模式·sqlserver
YHL3 天前
🎯 JavaScript 单例模式(Singleton Pattern)—— 从理论到实战
javascript·设计模式
2401_868534783 天前
网络环境规划核心考点全梳理
网络·设计模式
feiyu_gao3 天前
Cocreation Framework:一套给所有人的 AI 协作思考系统
设计模式·aigc·ai编程