设计模式 08 · 装饰器模式

上一篇代理模式结尾,我们埋了个钩子:代理和装饰器模式(Decorator) 代码结构几乎一样,但意图相反------代理管"控制",装饰器管"增强"。这一篇就来把装饰器讲透,顺便从另一头把这条界限夯实。

装饰器要解决的问题,一句话:在不修改原对象、也不靠继承的前提下,动态地给对象叠加新功能。 关键词是"动态"和"叠加"------你可以在运行时,像穿衣服一样,给一个对象一层层地包上新能力,想加哪层加哪层,顺序随意,还能自由组合。它最形象的比喻就是洋葱:核心是那个原始对象,外面一层层包裹着各种装饰,每一层都在前一层的基础上增强一点点。

这一篇我们用"订单价格计算"这个再典型不过的场景切入:一个订单的最终价格,可能要在原价基础上叠加会员折扣、再叠加满减、再叠加优惠券......而且不同订单叠加的组合千变万化。你会看到,如果用继承来应对这种"任意组合",会立刻陷入类爆炸的泥潭,而装饰器用"组合"优雅地化解了它------这正是第一篇"合成复用原则(组合优于继承)"最好的一个活教材。最后我们还会看 Java IO 流这个装饰器的教科书级应用,并把装饰器和代理彻底辨析清楚。

这篇文章按这条线索展开:先看"价格叠加"用继承会怎样类爆炸;再引出装饰器,用洋葱式包裹优雅解决;然后讲清它的四个角色和骨架;接着剖析它为什么是"组合优于继承"的最佳范例;再看 Java IO 这个最经典的装饰器实例;最后从另一头辨析装饰器与代理的区别,给出适用边界。贯穿例是订单价格的优惠叠加。

目录

  1. 一个"价格叠加"引发的类爆炸
  2. 装饰器模式:像洋葱一样层层包裹
  3. 四个角色与骨架
  4. 为什么装饰器是"组合优于继承"的活教材
  5. [Java IO:装饰器的教科书级应用](#Java IO:装饰器的教科书级应用)
  6. [装饰器 vs 代理:从另一头把界限夯实](#装饰器 vs 代理:从另一头把界限夯实)
  7. 什么时候用装饰器

一、一个"价格叠加"引发的类爆炸

我们的订单要算最终价格。基础是订单原价,但业务上有一堆可叠加的优惠:

  • 会员折扣(会员打 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ⁿ 级增长;
  • 静态、编译期定死 :一个订单一旦 newMemberOrder,它就永远是会员订单,运行时没法再给它加满减;
  • 组合关系写死在类里:想调整叠加顺序、动态增减优惠,统统做不到。

装饰器方案(组合)的优势,正好逐条对上:

  • 类的数量是线性的: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 为什么要这么设计?因为"数据来源"(文件、网络、内存、数组)和"处理能力"(缓冲、加密、压缩、编码转换)是两个可以任意组合的维度 。如果用继承,就会出现 BufferedFileInputStreamGzipBufferedFileInputStreamBufferedNetworkInputStream......又一次 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),适配器就是那个"转接头"。

相关推荐
董员外15 小时前
RAG 系统进化论(八):Agentic RAG(智能体式 RAG),从回答问题到完成任务
人工智能·后端·设计模式
董员外20 小时前
RAG 系统进化论(七):Multimodal RAG(多模态 RAG),当知识存在于表格、图片和页面中
人工智能·后端·设计模式
大山同学20 小时前
第 2 篇(03-04 章):代理设计原则与工具使用设计模式
设计模式
啦啦啦啦啦zzzz2 天前
设计模式:桥接模式和组合模式
c++·设计模式·组合模式·桥接模式
莫得感情 o2 天前
设计模式 03 · 工厂方法模式
设计模式·工厂方法模式
cyforkk2 天前
高并发核心设计模式:Single-Flight 原理与实战
设计模式
饼干哥哥3 天前
字节Seedance2.5终于上线,这次在收割谁?
人工智能·设计模式·前端框架
董员外3 天前
RAG 系统进化论(六):GraphRAG(基于知识图谱的 RAG),从相似文本走向实体关系
人工智能·后端·设计模式
鬼鬼鬼3 天前
从 Prompt 到 Harness:企业级 Agent 工程的完整演进之路
设计模式·架构·ai编程