跳转至

装饰器模式

从 Starbuzz Coffee 说起

Starbuzz 是增长最快的咖啡连锁店。他们的菜单系统有一个 Beverage 抽象类,每种咖啡是一个子类。问题是,咖啡可以加各种配料:牛奶、豆浆、摩卡、奶泡……每种组合都建一个子类,就会出现"类爆炸"——HouseBlendWithSoyMilkAndMochaDarkRoastWithWhipAndMocha 等几十上百个子类。

这违反了**开闭原则**:类应该对扩展开放,对修改关闭。继承是静态的,运行时无法改变一杯咖啡里有什么配料。

🔍 定义

装饰器模式(Decorator)动态地将责任附加到对象上。装饰器提供了比继承更灵活的功能扩展方式。

设计原则:类应该对扩展开放,对修改关闭。(开闭原则)

⚠️ 不使用装饰器存在的问题

用继承实现每种组合,子类数量爆炸式增长:

DecoratorBadExample.java
package com.example.structural.decorator;

/**
 * 装饰器模式 - 反例
 * 问题:用继承组合功能,导致子类爆炸(N种饮料 × M种配料 = N×M个类)
 */
public class DecoratorBadExample {
    public static void main(String[] args) {
        // ❌ 每种组合都是一个独立子类
        BeverageBad e = new EspressoBad();
        System.out.println(e.getDescription() + " - ¥" + e.cost());

        BeverageBad em = new EspressoWithMilkBad();
        System.out.println(em.getDescription() + " - ¥" + em.cost());

        BeverageBad ems = new EspressoWithMilkAndSugarBad();
        System.out.println(ems.getDescription() + " - ¥" + ems.cost());

        // 如果加一种新配料(例如香草),需要新增所有组合的子类 ❌
    }
}

abstract class BeverageBad {
    public abstract String getDescription();
    public abstract double cost();
}

class EspressoBad extends BeverageBad {
    @Override public String getDescription() { return "浓缩咖啡"; }
    @Override public double cost()           { return 10.0; }
}

// ❌ 为每种组合创建子类
class EspressoWithMilkBad extends BeverageBad {
    @Override public String getDescription() { return "浓缩咖啡 + 牛奶"; }
    @Override public double cost()           { return 10.0 + 2.0; }
}

class EspressoWithMilkAndSugarBad extends BeverageBad {
    @Override public String getDescription() { return "浓缩咖啡 + 牛奶 + 糖"; }
    @Override public double cost()           { return 10.0 + 2.0 + 0.5; }
}

// 美式咖啡系列(同样要重复所有组合 ❌)
class AmericanoBad extends BeverageBad {
    @Override public String getDescription() { return "美式咖啡"; }
    @Override public double cost()           { return 8.0; }
}
// AmericanoWithMilkBad, AmericanoWithSugarBad, AmericanoWithMilkAndSugarBad ... 无限增长 ❌

🏗️ 设计模式结构(Starbuzz 咖啡)

%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
    classDef default fill:transparent,stroke:#768390
    class Beverage {
        <<abstract>>
        #description: String
        +getDescription() String
        +cost() double*
    }
    class Espresso {
        +cost() double
    }
    class DarkRoast {
        +cost() double
    }
    class HouseBlend {
        +cost() double
    }
    class CondimentDecorator {
        <<abstract>>
        #beverage: Beverage
        +getDescription() String*
    }
    class Mocha {
        +getDescription() String
        +cost() double
    }
    class Whip {
        +getDescription() String
        +cost() double
    }
    class Milk {
        +getDescription() String
        +cost() double
    }
    Beverage <|-- Espresso
    Beverage <|-- DarkRoast
    Beverage <|-- HouseBlend
    Beverage <|-- CondimentDecorator
    CondimentDecorator o--> Beverage
    CondimentDecorator <|-- Mocha
    CondimentDecorator <|-- Whip
    CondimentDecorator <|-- Milk
    note for Beverage "抽象组件(Component)"
    note for Espresso "具体组件(ConcreteComponent)"
    note for CondimentDecorator "抽象装饰器(Decorator)"
    note for Mocha "具体装饰器(ConcreteDecorator)"

装饰器(CondimentDecorator)继承 Beverage,并**持有一个 Beverage 对象**。这里的继承不是为了复用代码,而是为了保持相同的类型,让装饰器可以无缝替换被装饰的对象。

💻 设计模式举例说明

DecoratorExample.java
package com.example.structural.decorator;

/**
 * 装饰器模式 - 正例
 * 装饰器动态叠加功能,新增配料只需一个新装饰器类,不产生子类爆炸
 */
public class DecoratorExample {
    public static void main(String[] args) {
        // ✅ 用装饰器动态叠加:浓缩咖啡 + 牛奶 + 糖
        Beverage espresso = new Espresso();
        espresso = new MilkDecorator(espresso);
        espresso = new SugarDecorator(espresso);
        System.out.println(espresso.getDescription() + " - ¥" + espresso.cost());
        // 输出: 浓缩咖啡, 牛奶, 糖 - ¥12.5

        // ✅ 美式咖啡 + 双份糖,只需重新组合,不用新增类
        Beverage americano = new Americano();
        americano = new SugarDecorator(americano);
        americano = new SugarDecorator(americano); // 双份糖
        System.out.println(americano.getDescription() + " - ¥" + americano.cost());
    }
}

// 饮料组件接口
abstract class Beverage {
    public abstract String getDescription();
    public abstract double cost();
}

// 具体组件:浓缩咖啡
class Espresso extends Beverage {
    @Override public String getDescription() { return "浓缩咖啡"; }
    @Override public double cost()           { return 10.0; }
}

// 具体组件:美式咖啡
class Americano extends Beverage {
    @Override public String getDescription() { return "美式咖啡"; }
    @Override public double cost()           { return 8.0; }
}

// 抽象装饰器:持有被装饰对象的引用
abstract class CondimentDecorator extends Beverage {
    protected final Beverage wrapped;
    public CondimentDecorator(Beverage beverage) { this.wrapped = beverage; }
}

// ✅ 具体装饰器:牛奶(只需一个类,可叠加到任何饮料)
class MilkDecorator extends CondimentDecorator {
    public MilkDecorator(Beverage beverage) { super(beverage); }

    @Override public String getDescription() { return wrapped.getDescription() + ", 牛奶"; }
    @Override public double cost()           { return wrapped.cost() + 2.0; }
}

// ✅ 具体装饰器:糖
class SugarDecorator extends CondimentDecorator {
    public SugarDecorator(Beverage beverage) { super(beverage); }

    @Override public String getDescription() { return wrapped.getDescription() + ", 糖"; }
    @Override public double cost()           { return wrapped.cost() + 0.5; }
}

双倍摩卡的实现

想要双倍摩卡?直接包两层:new Mocha(new Mocha(darkRoast))。这是装饰器模式的精妙之处——同一个装饰器可以叠加多次,且每层都叠加自己的 cost()

⚖️ 优缺点

优点:

  • 避免继承爆炸,类数量大幅减少
  • 运行时动态叠加任意功能组合
  • 符合**单一职责原则**:每个装饰器只负责一种配料
  • 符合**开闭原则**:新增配料只需新建一个装饰器类

缺点:

  • 多层装饰器叠套后,调试时调用链较长
  • 如果某层装饰器持有状态,顺序不同会影响结果
  • 代码中会出现很多小类

🔗 与其它模式的关系

模式 接口变化? 对象来源 主要意图
装饰器(Decorator) ❌ 不变 由调用方传入 动态增强功能
代理(Proxy) ❌ 不变 代理自己创建/管理 控制访问
适配器(Adapter) ✅ 改变 兼容接口

🗂️ 应用场景

  • 需要动态给对象添加功能,且功能组合多变
  • 不能通过继承扩展(如 final 类)
  • JDK:InputStream → FileInputStream → BufferedInputStream → DataInputStream 就是经典装饰器链
  • Spring:HttpServletRequestWrapper

🏭 工业视角

Java IO 是教科书级装饰器实现,但藏着一个设计细节

王争对 Java IO 源码的剖析揭示了一个常被忽略的问题:BufferedInputStreamDataInputStream 并不直接继承 InputStream,而是继承 FilterInputStream。这个中间类并非多余——它解决了**委托传递**的代码重复问题:

FilterInputStream:装饰器父类,默认委托所有方法
public class FilterInputStream extends InputStream {
    protected volatile InputStream in; // 持有被装饰对象

    // 对每个方法默认委托,子类只需覆写需要增强的方法
    public int read() throws IOException { return in.read(); }
    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
    public long skip(long n) throws IOException { return in.skip(n); }
    public void close() throws IOException { in.close(); }
    // ...
}

// BufferedInputStream 只需覆写需要缓存增强的方法,其余继承 FilterInputStream
public class BufferedInputStream extends FilterInputStream {
    protected BufferedInputStream(InputStream in) { super(in); }
    // 只覆写 read()、mark()、reset() 等需要缓冲逻辑的方法
}

为什么不能让 BufferedInputStream 直接继承 InputStream?

InputStream 是抽象类,有默认实现。若 BufferedInputStream 直接继承并不覆写某个方法,调用时走的是 InputStream 的默认逻辑,而**不会**委托给构造函数传入的 FileInputStream,导致功能错乱。FilterInputStream 的价值正在于此:它统一做了一层默认委托,让子类只需关注真正需要增强的方法。

装饰器 vs 继承:运行时组合 vs 编译时固定

装饰器模式本质上是用**对象组合**替代**类继承**来实现功能扩展,两者的对比在 Java IO 上最直观:

方式 扩展时机 组合方式 类数量
继承 编译期,固定 每种组合一个子类 指数级增长(BufferedDataFileInputStream 等)
装饰器 运行时,动态 按需嵌套包装 线性增长,每种能力一个装饰器类
装饰器链:运行时按需叠加能力
1
2
3
4
5
// 既要缓冲读取,又要按基本类型读取 —— 运行时嵌套,无需新建子类
InputStream in   = new FileInputStream("/data/file.bin");
InputStream bin  = new BufferedInputStream(in);   // 第一层增强:缓冲
DataInputStream din = new DataInputStream(bin);    // 第二层增强:基本类型读取
int value = din.readInt();

装饰器的缺点:调试时难以看清调用链

层层包装之后,一个对象实际上被套了几层装饰器,运行时不容易直观感知。出现 bug 时需要逐层确认当前对象的真实类型(可用 getClass() 或断点查看)。另外,如果多个装饰器持有独立状态,叠加顺序不同可能产生不同结果,使用时需注意文档说明。