装饰器模式(Decorator Pattern)

装饰器模式是一种结构型设计模式,它允许在不修改原有对象结构的情况下,通过动态地“包装”对象来为其添加新的行为或功能。这种模式类似于“层层包裹”,每个包装层(装饰器)可以增强或修改被装饰的对象,而不影响其他同类对象。

为什么使用装饰器模式?
  • 问题解决:当你需要为一个类添加新功能时,如果直接修改类,会违反开闭原则(对扩展开放,对修改关闭)。继承虽然可行,但会导致类爆炸(太多子类)。
  • 适用场景
    • 需要动态添加或移除功能。
    • 需要在运行时组合多个功能。
    • 例如:UI组件的边框、滚动条添加;IO流的包装(如BufferedInputStream)。
  • 优点
    • 灵活性高,支持组合。
    • 符合开闭原则。
  • 缺点
    • 可能产生过多小对象,增加系统复杂性。
    • 调试时调试链较难。
核心结构

装饰器模式涉及四个主要角色:

  • Component(组件):抽象组件,定义接口。
  • ConcreteComponent(具体组件):实现基本功能的对象。
  • Decorator(装饰器):持有Component引用,提供装饰接口。
  • ConcreteDecorator(具体装饰器):实现具体的装饰逻辑,通常调用Component的方法并添加额外行为。

UML类图描述(文本表示):

Component (抽象)
  ↑
ConcreteComponent (具体组件)
  ↑
Decorator (装饰器) ──> Component
  ↑
ConcreteDecoratorA/B (具体装饰器)
经典示例:咖啡店点单系统

假设我们有一个咖啡店系统,基本咖啡有价格,装饰器可以添加牛奶、糖等配料动态计算总价。

装饰器模式 Java 实现示例

以下是装饰器模式在 Java 中的经典实现,使用咖啡店点单系统作为示例。结构与 Python 版本类似:定义接口、具体组件、抽象装饰器和具体装饰器。通过动态包装,可以灵活添加配料并计算总价。

核心代码
// 1. 组件接口
interface Coffee {
    double getCost();
    String getDescription();
}

// 2. 具体组件
class SimpleCoffee implements Coffee {
    @Override
    public double getCost() {
        return 5.0;
    }

    @Override
    public String getDescription() {
        return "Simple Coffee";
    }
}

// 3. 抽象装饰器
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public double getCost() {
        return coffee.getCost();
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }
}

// 4. 具体装饰器:牛奶
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return super.getCost() + 2.0;
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", Milk";
    }
}

// 5. 具体装饰器:糖
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return super.getCost() + 1.0;
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", Sugar";
    }
}

// 使用示例
public class DecoratorDemo {
    public static void main(String[] args) {
        // 基本咖啡
        Coffee coffee = new SimpleCoffee();
        System.out.println(coffee.getDescription() + ": $" + coffee.getCost());

        // 添加牛奶
        Coffee coffeeWithMilk = new MilkDecorator(coffee);
        System.out.println(coffeeWithMilk.getDescription() + ": $" + coffeeWithMilk.getCost());

        // 添加牛奶和糖
        Coffee coffeeWithMilkSugar = new SugarDecorator(coffeeWithMilk);
        System.out.println(coffeeWithMilkSugar.getDescription() + ": $" + coffeeWithMilkSugar.getCost());
    }
}
运行输出
Simple Coffee: $5.0
Simple Coffee, Milk: $7.0
Simple Coffee, Milk, Sugar: $8.0

这个 Java 实现利用接口和继承,展示了装饰器的透明性:装饰后的对象仍可作为 Coffee 类型使用。如果需要编译运行或其他变体(如泛型支持),可以进一步调整!
``

这个例子展示了如何通过层层装饰动态构建对象,而无需创建大量子类。

与其他模式的比较
模式区别与联系
适配器适配器转换接口,装饰器增强功能。
代理代理控制访问,装饰器添加行为。
组合组合处理树状结构,装饰器是线性包装。

装饰器模式的优势展示:通过对比与实际场景

咖啡店的简单示例更像是入门演示,它展示了基本结构,但确实没突出装饰器模式的“杀手级”优势——动态组合、避免类爆炸、符合开闭原则。下面我来通过对比继承方式一个更真实的Java IO流示例,来直观展示这些优势。继承方式会让你看到“噩梦”:如果有N种基础对象和M种装饰,每种组合都需要一个子类,导致2^(N*M)级的类爆炸!装饰器只需O(N+M)个类,就能无限组合。

1. 对比:继承 vs 装饰器(以咖啡店扩展为例)

假设基础咖啡有3种(Espresso、Latte、Mocha),装饰有4种(Milk、Sugar、Whip、Soy)。如果用继承

  • 需要为每种基础+每种装饰组合创建子类:3 * 2^4 = 48个子类!(二进制组合,每种装饰可选)。
  • 代码维护地狱:新增一种装饰(如Chocolate),需改48个类,违反开闭原则。

装饰器:只需3个基础类 + 1个抽象装饰器 + 4个具体装饰器 = 8个类。运行时动态包装,就能生成任意组合(如Latte + Milk + Whip)。

伪代码对比(Java风格)

  • 继承方式(简化,只展示部分,实际会爆炸)
    class Espresso {}  // 基础
    class EspressoWithMilk extends Espresso {}  // +Milk
    class EspressoWithMilkAndSugar extends EspressoWithMilk {}  // +Sugar
    // ... 继续下去,48个类...
    // 新增Chocolate?重写所有48个!
    
  • 装饰器方式(之前示例扩展)
    // 只需新增一个ChocolateDecorator
    class ChocolateDecorator extends CoffeeDecorator {
        public ChocolateDecorator(Coffee coffee) { super(coffee); }
        @Override public double getCost() { return super.getCost() + 1.5; }
        @Override public String getDescription() { return super.getDescription() + ", Chocolate"; }
    }
    
    // 使用:动态组合,无需新类
    Coffee fancy = new ChocolateDecorator(new WhipDecorator(new MilkDecorator(new Latte())));
    // 输出: Latte, Milk, Whip, Chocolate: $12.5
    

优势一览:

  • 灵活性:运行时决定组合(如用户点单时加Chocolate),无需编译新类。
  • 可维护:新增装饰只需1个类,不碰原有代码。
  • 内存高效:共享基础对象,避免重复实现。
2. 真实场景:Java IO流中的装饰器(BufferedInputStream等)

Java标准库就是装饰器模式的教科书!InputStream是抽象组件,FileInputStream是具体组件,BufferedInputStreamDataInputStream等是装饰器。优势:无需为“文件+缓冲+数据”创建专用类,直接层层包装。

为什么突出优势?

  • 问题:读文件时,可能需要缓冲(加速)、加密(安全)、压缩(节省)。继承会产生海量子类(如FileBufferedEncryptedStream…)。
  • 装饰器解决:动态包装,组合任意功能,且透明(包装后仍用InputStream接口)。
  • 实际收益:Java IO库只需几十个类,就能支持无限IO变体;性能高(缓冲减少系统调用)。

Java代码实现(简化版,模拟文件读)

import java.io.*;

// 1. 组件接口(Java已有)
abstract class InputStream {  // 简化,实际是java.io.InputStream
    public abstract int read() throws IOException;
}

// 2. 具体组件:文件流
class FileInputStream extends InputStream {
    private String fileContent = "Hello, World!";  // 模拟文件内容
    private int index = 0;

    @Override
    public int read() throws IOException {
        if (index < fileContent.length()) {
            return fileContent.charAt(index++);
        }
        return -1;  // EOF
    }
}

// 3. 抽象装饰器
abstract class FilterInputStream extends InputStream {
    protected InputStream in;

    public FilterInputStream(InputStream in) {
        this.in = in;
    }

    @Override
    public int read() throws IOException {
        return in.read();
    }
}

// 4. 具体装饰器:缓冲(加速读)
class BufferedInputStream extends FilterInputStream {
    private byte[] buf = new byte[1024];
    private int count = 0, pos = 0;

    public BufferedInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read() throws IOException {
        if (pos >= count) {
            count = in.read(buf);  // 批量读
            pos = 0;
            if (count == -1) return -1;
        }
        return buf[pos++] & 0xFF;  // 模拟缓冲优势
    }
}

// 5. 另一个具体装饰器:数据流(读int等结构化数据)
class DataInputStream extends FilterInputStream {
    public DataInputStream(InputStream in) {
        super(in);
    }

    public int readInt() throws IOException {  // 添加新方法:读int
        int b1 = read(), b2 = read(), b3 = read(), b4 = read();
        if ((b1 | b2 | b3 | b4) < 0) throw new IOException("EOF");
        return (b1 << 24) + (b2 << 16) + (b3 << 8) + b4;
    }
}

// 使用示例(突出动态组合)
public class IODecoratorDemo {
    public static void main(String[] args) throws IOException {
        // 基础:直接读文件(慢,无缓冲)
        InputStream file = new FileInputStream();
        System.out.println("基础读: " + (char) file.read());  // H (每次系统调用,慢)

        // 组合1:加缓冲(加速)
        InputStream buffered = new BufferedInputStream(file);
        System.out.println("缓冲读: " + (char) buffered.read());  // H (批量读,快)

        // 组合2:缓冲 + 数据流(加速 + 结构化读)
        InputStream dataBuffered = new DataInputStream(new BufferedInputStream(file));
        // 假设文件有int数据,读int而非字节
        // int value = ((DataInputStream) dataBuffered).readInt();  // 新功能!

        // 优势:新增加密装饰器?只需1类,包装dataBuffered即可。
        // 无需改FileInputStream!
    }
}

运行洞察(手动模拟输出):

  • 基础读:每次read()都“系统调用”,文件大时超慢。
  • 缓冲后:批量读,I/O调用减99%,性能飞起。
  • 加数据流:还能读int/long等,无需改底层文件流。

这个IO示例展示了实际工程优势:在高性能系统(如服务器日志读)中,装饰器让代码简洁、可扩展。如果不用,它会是继承的“类工厂”灾难。

怎么判断什么时候使用装饰器模式?

装饰器模式的核心价值在于动态扩展对象行为,而非静态继承。它适合那些“可选、组合、可运行时调整”的场景。下面我从几个实用维度帮你判断:通过检查你的问题是否匹配这些条件,就能决定是否引入装饰器。如果是“静态、固定、少量变体”的需求,可能继承或直接修改更简单。

主要判断标准

以下是基于设计模式经典指南(如GoF书)和实践经验的判断点。我用表格总结,便于对比(每个点后附引用来源):

判断标准详细说明示例场景
需要运行时动态添加/移除功能如果功能不是编译时决定的,而是运行时根据条件(如用户输入)组合(如加“牛奶”或“糖”),装饰器能层层包装,而不需预定义所有组合。Java IO流:基础文件流 + 运行时加缓冲(BufferedInputStream)或加密。
避免类爆炸(继承过多子类)如果基础类有N种,装饰有M种,继承需创建N×2^M个子类;装饰器只需N+M个类,就能无限组合。咖啡店:3种咖啡 + 4种配料,用继承=48类;装饰器=7类搞定。
符合开闭原则(扩展不改原代码)新功能只需加新装饰器类,不碰原有组件代码。UI组件:基础按钮 + 运行时加边框/滚动条,不改按钮类。
遵守单一职责原则每个类只管一件事,装饰器将独立行为(如“加日志”)分离成小类,便于复用。日志系统:核心业务 + 装饰加文件/数据库日志,各司其职。
继承不可用或不合适当基类是第三方库(不能继承)或继承会导致紧耦合时,用装饰器包装。第三方API:包装响应加缓存,而不继承其类。
快速决策流程
  1. 问自己:功能是“可选组合”还是“必选固定”?如果是前者(如插件系统),用装饰器。
  2. 检查复杂度:变体>5种?继承会爆炸→装饰器。
  3. 测试可行性:如果包装后对象仍需透明使用(如统一接口),完美匹配。
  4. 反例(何时不用):变体少、静态绑定(如简单if-else),用继承或策略模式更轻量。

实际中,Java IO库就是最佳证明:它用装饰器处理无数IO变体,避免了“继承地狱”。

更多推荐