一天一个设计模式——装饰器模式
装饰器模式(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 {@Overridepublic double getCost() {return 5.0;}@Overridepublic String getDescription() {return "Simple Coffee";}
}// 3. 抽象装饰器
abstract class CoffeeDecorator implements Coffee {protected Coffee coffee;public CoffeeDecorator(Coffee coffee) {this.coffee = coffee;}@Overridepublic double getCost() {return coffee.getCost();}@Overridepublic String getDescription() {return coffee.getDescription();}
}// 4. 具体装饰器:牛奶
class MilkDecorator extends CoffeeDecorator {public MilkDecorator(Coffee coffee) {super(coffee);}@Overridepublic double getCost() {return super.getCost() + 2.0;}@Overridepublic String getDescription() {return super.getDescription() + ", Milk";}
}// 5. 具体装饰器:糖
class SugarDecorator extends CoffeeDecorator {public SugarDecorator(Coffee coffee) {super(coffee);}@Overridepublic double getCost() {return super.getCost() + 1.0;}@Overridepublic 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
是具体组件,BufferedInputStream
、DataInputStream
等是装饰器。优势:无需为“文件+缓冲+数据”创建专用类,直接层层包装。
为什么突出优势?
- 问题:读文件时,可能需要缓冲(加速)、加密(安全)、压缩(节省)。继承会产生海量子类(如FileBufferedEncryptedStream…)。
- 装饰器解决:动态包装,组合任意功能,且透明(包装后仍用InputStream接口)。
- 实际收益:Java IO库只需几十个类,就能支持无限IO变体;性能高(缓冲减少系统调用)。
Java代码实现(简化版,模拟文件读):
import java.io.*;// 1. 组件接口(Java已有)
abstract class InputStream { // 简化,实际是java.io.InputStreampublic abstract int read() throws IOException;
}// 2. 具体组件:文件流
class FileInputStream extends InputStream {private String fileContent = "Hello, World!"; // 模拟文件内容private int index = 0;@Overridepublic 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;}@Overridepublic 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);}@Overridepublic 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 { // 添加新方法:读intint 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:包装响应加缓存,而不继承其类。 |
快速决策流程
- 问自己:功能是“可选组合”还是“必选固定”?如果是前者(如插件系统),用装饰器。
- 检查复杂度:变体>5种?继承会爆炸→装饰器。
- 测试可行性:如果包装后对象仍需透明使用(如统一接口),完美匹配。
- 反例(何时不用):变体少、静态绑定(如简单if-else),用继承或策略模式更轻量。
实际中,Java IO库就是最佳证明:它用装饰器处理无数IO变体,避免了“继承地狱”。