深入浅出设计模式【九、装饰器模式】
一、装饰器模式介绍
装饰器模式的核心思想是在不改变现有对象结构的情况下,动态地、透明地(对客户端而言)为对象添加新的功能。
当使用继承来扩展功能时,类的行为在编译时就被静态地确定了。如果需要多种功能的组合,就会导致“子类爆炸”问题(例如,一个 Beverage 类,如果有 Milk, Soy, Mocha, Whip 等配料,通过继承会产生 DarkRoastWithMilkAndMocha, DarkRoastWithSoyAndWhip 等大量子类)。
装饰器模式通过定义一系列包装对象(装饰器)来解决这个问题。每个装饰器都包装原始对象(或其他装饰器),并在保持原始接口的前提下,提供额外的功能。客户端可以递归地包装对象,从而实现功能的灵活组合。
二、核心概念与意图
-
核心概念:
- 组件 (Component): 定义一个对象接口,可以动态地给这些对象添加职责。它是被装饰对象和装饰器对象的共同超类或接口。
- 具体组件 (Concrete Component): 定义了一个具体的对象,即被装饰的原始对象。它实现了
Component接口。 - 装饰器 (Decorator): 持有一个
Component对象的引用,并实现(或继承)Component接口。它是所有具体装饰器的公共父类(通常是抽象类),定义了装饰器的基本结构。 - 具体装饰器 (Concrete Decorator): 向组件(被装饰对象)添加具体的职责。每个具体装饰器都包装了一个
Component对象,并在其操作前后添加自己的行为。
-
意图:
- 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
- 避免由于子类化而导致的类爆炸问题。
- 保持类的单一职责原则,将核心职责与装饰功能分离。
三、适用场景剖析
装饰器模式在以下场景中非常有效:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责: 当不能或不希望使用继承来扩展功能时(例如,类被标记为
final,或者扩展会导致类层次结构过于复杂)。 - 需要动态地添加、撤销或修改对象的功能: 装饰器可以在运行时被添加和移除,提供了极大的灵活性。
- 需要大量可组合的功能: 当有大量独立的功能扩展,并且这些功能需要以各种方式组合使用时(如文本格式、IO流处理、UI组件装饰)。
- 对扩展开放,对修改关闭 (Open/Closed Principle): 可以通过创建新的装饰器类来扩展系统,而无需修改现有的组件和装饰器代码。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了装饰器模式的结构和角色间的关系,特别是其“包装”和“递归组合”的核心机制:
Component(组件): 定义了所有组件(被装饰对象和装饰器)的公共接口(如operation())。客户端只依赖于这个接口。ConcreteComponent(具体组件): 是被装饰的原始对象。它定义了核心的、基本的功能。Decorator(装饰器): 模式的核心。- 它持有一个对
Component对象的引用 (-component: Component)。这个引用可以指向一个ConcreteComponent,也可以指向另一个Decorator,从而形成递归的包装链。 - 它实现(或继承)了
Component接口。这使得Decorator及其子类在外观上与ConcreteComponent一致,对客户端是透明的。 - 它的
operation()方法通常会调用所引用component的operation()方法(即委托),并可能在此前后添加一些额外的行为。
- 它持有一个对
ConcreteDecoratorA,ConcreteDecoratorB(具体装饰器):- 继承自
Decorator。 - 在重写的
operation()方法中,除了调用父类的operation()(即委托给被包装对象),还会添加自己特有的功能(addedBehavior())。 - 可以添加新的方法,但为了保持“透明性”,通常应谨慎,以免客户端需要依赖具体装饰器类型。
- 继承自
Client(客户端): 通过Component接口与对象交互。它不知道也不需要知道它处理的是核心对象还是被装饰过的对象。它负责组装装饰链(例如:component = new ConcreteDecoratorB(new ConcreteDecoratorA(new ConcreteComponent())))。
五、各种实现方式及其优缺点
装饰器模式的实现相对标准,但其设计思想比实现方式更重要。
1. 标准实现(接口+抽象类)
即上述UML所描述的方式,这是最经典和推荐的方式。
// 1. Component Interface
public interface DataSource {
void writeData(String data);
String readData();
}
// 2. Concrete Component
public class FileDataSource implements DataSource {
private String filename;
// ... constructor, file operations ...
@Override
public void writeData(String data) {
System.out.println("Writing " + data + " to file.");
// Actual file write logic
}
@Override
public String readData() {
System.out.println("Reading data from file.");
return "data";
}
}
// 3. Base Decorator (Abstract class implementing the interface)
public abstract class DataSourceDecorator implements DataSource {
protected DataSource wrappee; // The component being decorated
public DataSourceDecorator(DataSource source) {
this.wrappee = source;
}
@Override
public void writeData(String data) {
wrappee.writeData(data); // Delegate to the wrappee
}
@Override
public String readData() {
return wrappee.readData(); // Delegate to the wrappee
}
}
// 4. Concrete Decorators
public class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
String encryptedData = "ENCRYPTED(" + data + ")"; // Simulate encryption
super.writeData(encryptedData); // Delegate with encrypted data
}
@Override
public String readData() {
String data = super.readData(); // Read the (encrypted) data
return data.replace("ENCRYPTED(", "").replace(")", ""); // Simulate decryption
}
}
public class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
String compressedData = "COMPRESSED(" + data + ")"; // Simulate compression
super.writeData(compressedData);
}
@Override
public String readData() {
String data = super.readData();
return data.replace("COMPRESSED(", "").replace(")", ""); // Simulate decompression
}
}
// 5. Client Code
public class Client {
public static void main(String[] args) {
// Original component
DataSource source = new FileDataSource("data.txt");
// Dynamically add responsibilities
DataSource encryptedSource = new EncryptionDecorator(source);
DataSource compressedAndEncryptedSource = new CompressionDecorator(encryptedSource);
// Client uses the decorated object transparently
compressedAndEncryptedSource.writeData("Secret Data");
// Output: Writing COMPRESSED(ENCRYPTED(Secret Data)) to file.
String result = compressedAndEncryptedSource.readData();
// Output: Reading data from file.
// result will be "Secret Data" after decompression and decryption
}
}
- 优点:
- 比继承更灵活: 功能可以动态地添加和撤销,支持功能的无限组合。
- 避免类爆炸: 使用少量装饰器类,通过组合可以创造出大量不同的行为组合。
- 符合开闭原则和单一职责原则: 可以引入新的装饰器而不修改现有代码;每个装饰器类只负责一个特定的功能。
- 缺点:
- 设计复杂度增加: 会引入大量小类,使系统变得复杂,难以理解和调试。
- 初始化配置复杂: 组装装饰链的代码可能变得冗长且难以维护(可通过工厂模式、建造者模式缓解)。
- 难以维护装饰器的顺序: 如果装饰器的包装顺序对结果有影响,需要客户端小心管理。
六、最佳实践
- 保持接口一致性: 装饰器必须与其装饰的对象实现相同的接口(
Component)。这是实现“透明性”的关键,确保客户端无法区分原始对象和装饰后的对象。 - 装饰器应专注于添加功能: 装饰器的核心职责是“装饰”,它应该委托核心操作给被包装对象,然后添加边缘行为。避免在装饰器中实现核心业务逻辑。
- 使用抽象基类装饰器: 定义一个抽象的
Decorator基类(如DataSourceDecorator)来实现接口并将操作委托给被包装对象。这简化了具体装饰器的实现,它们只需重写需要增强的方法。 - 谨慎添加新方法: 在装饰器中添加新的公共方法会破坏透明性,因为客户端需要向下转型为具体装饰器类型才能使用它们。如果必须添加新行为,考虑是否应该使用策略模式等其他模式。
- 与代理模式区分:
- 装饰器模式: 目的是增强功能。控制是分散的,由一系列装饰器共同完成。客户端通常负责组装装饰链。
- 代理模式: 目的是控制访问(如延迟加载、访问控制、日志记录)。代理通常直接管理其服务对象的生命周期,并且代理和目标对象的关系通常在编译时就已经确定(或由代理内部决定),对客户端是隐藏的。
- 两者在结构上非常相似,但意图是区分的核心。
七、在开发中的演变和应用
装饰器模式的思想是现代框架中实现可扩展性和横切关注点(Cross-Cutting Concerns)的标准手段:
- Java I/O Streams: 这是装饰器模式最经典、最教科书式的应用,如前所述。
- Servlet API Wrappers: 在Java Web开发中,
HttpServletRequestWrapper和HttpServletResponseWrapper是装饰器模式的典型应用。它们允许开发者编写装饰器来包装原始的请求和响应对象,以实现输入/输出的过滤、修改(如压缩、加密)、记录日志等功能,而无需修改原始Servlet代码。Servlet Filter 链就是基于此构建的。 - 面向切面编程 (AOP): AOP框架(如Spring AOP)的本质可以看作是在运行时动态创建代理/装饰器,将切面(如事务管理
@Transactional、日志@Logging)织入到目标方法的前后。这是一种更高级、更自动化的装饰器应用。 - GUI工具包: 为可视组件添加边框、滚动条、工具栏等,通常使用装饰器模式实现。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java I/O (
java.iopackage):Component:InputStream,OutputStream,Reader,Writer(抽象类)。ConcreteComponent:FileInputStream,FileOutputStream,FileReader,FileWriter,ByteArrayInputStream等。Decorator:FilterInputStream,FilterOutputStream,FilterReader,FilterWriter(抽象类)。ConcreteDecorator:BufferedInputStream,BufferedOutputStream(添加缓冲功能)。DataInputStream,DataOutputStream(添加读写基本数据类型的功能)。GZIPInputStream,GZIPOutputStream(添加压缩/解压功能)。InputStreamReader,OutputStreamWriter(桥接字节流到字符流,虽是适配器,但也具装饰性)。
- 客户端使用:
// Transparent chaining of decorators InputStream inputStream = new BufferedInputStream( // Decoration: Buffering new GZIPInputStream( // Decoration: Decompression new FileInputStream("data.gz"))); // Core: File access // Client code only deals with the InputStream interface int data = inputStream.read();
-
Java Collections -
java.util.Collections:- 方法如
Collections.unmodifiableList(List list),Collections.synchronizedList(List list),Collections.checkedList(List list, Class type)。 - 这些方法返回的包装类对象就是装饰器。它们包装了原始的集合对象,添加了不可修改、线程安全、类型检查等行为,而客户端仍然通过
List接口与之交互。
- 方法如
-
Spring Framework -
BeanDefinitionDecorator:- 在Spring的XML配置解析中,用于装饰Bean定义,允许在解析过程中修改或增强Bean的定义信息。
-
MyBatis - Cache Decorators:
- MyBatis的缓存模块使用了装饰器模式。
PerpetualCache是基本的缓存实现(具体组件),而LruCache,FifoCache,ScheduledCache,LoggingCache,SynchronizedCache等都是装饰器,为基本缓存添加了LRU淘汰、FIFO淘汰、定时清空、日志、同步等功能。可以灵活地组合这些装饰器来构建所需的缓存。
- MyBatis的缓存模块使用了装饰器模式。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 结构型设计模式 |
| 核心意图 | 动态地、透明地给对象添加额外的职责,是继承的替代方案。 |
| 关键角色 | 组件(Component), 具体组件(ConcreteComponent), 装饰器(Decorator), 具体装饰器(ConcreteDecorator) |
| 核心机制 | 1. 实现相同接口。 2. 包装(组合): 装饰器持有组件引用。 3. 委托与增强: 在调用被包装对象方法前后添加行为。 |
| 主要优点 | 1. 极佳的灵活性: 动态添加/撤销功能,避免子类爆炸。 2. 符合开闭原则: 易于扩展新装饰器。 3. 保持单一职责: 功能分解到小类。 |
| 主要缺点 | 1. 复杂度增加: 大量小类,调试困难。 2. 初始化复杂: 组装装饰链的代码可能冗长。 |
| 适用场景 | 需要动态、透明地扩展对象功能,且不宜使用继承或继承会导致类爆炸的场景(IO处理、UI增强、功能组合、横切关注点)。 |
| 最佳实践 | 保持接口透明;使用抽象基类装饰器;优先组合而非继承;与代理模式区分意图。 |
| 关系与对比 | vs. 适配器: 装饰器不改变接口但增强功能;适配器改变接口但不增强功能。 vs. 代理: 装饰器控制分散,目的为增强;代理控制集中,目的为访问控制。结构相似,意图不同。 |
| 真实世界应用 | Java I/O Streams (经典)。Java Collections 的包装方法。Servlet Wrappers。MyBatis Caching。Spring AOP (思想)。 |
装饰器模式通过巧妙的组合和委托机制,提供了一种极其灵活和符合设计原则的功能扩展方式。它是解决“继承僵化”和“类爆炸”问题的利器,在需要为对象动态添加职责的场景下,是架构师和开发者的首选方案。深入理解装饰器模式,对于编写可维护、可扩展的高质量代码至关重要。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120878

浙公网安备 33010602011771号