装饰器模式是一种结构型设计模式,在不改变原始类接口的前提下,创建了一个装饰类,装饰类和原始类实现相同的接口,然后通过组合将装饰类包装原始类,在保持类方法签名完整性的前提下,提供了额外的功能。
接下来,我们通过装饰器模式的经典运用,Java I/O类库,来介绍装饰器模式。
当我们要读取一个文档的时候,通常会采用下面的写法。
public class App {
public static void main(String[] args) throws Exception {
InputStream in = new FileInputStream("D:\\面试题目.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
int length = 0;
while ((length = bin.read(data)) != -1) {
System.out.println(new String(data, 0, length));
}
}
}
InputStream 是一个字节流输入抽象类,FileInputStream 是专门用来读取文件流的子类,BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率。
初看上面的代码会觉得用法非常麻烦,会觉得如果直接定义一个BufferedFileInputStream子类实现FileInputStream会更加简洁和易用,如下所示(BufferedFileInputStream实际上不存在,以下代码无法通过编译)。
class App {
public static void main(String[] args) throws Exception {
InputStream bin = new BufferedFileInputStream("D:\\面试题目.txt");
byte[] data = new byte[128];
int length = 0;
while ((length = bin.read(data)) != -1) {
System.out.println(new String(data, 0, length));
}
}
}
但实际上,继承 InputStream 的子类有很多,如果我们给每一个 InputStream 的子类,再继续派生支持缓存区读取的子类。除了支持缓存区读取之外,可能我们还需要支持按照基本数据类型(int、boolean、long 等)来读取的数据输入流。这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出更多的类,类继承结构变得无比复杂,代码既不好扩展,也不好维护。
基于面向对象中多用组合少用继承的设计原则,Java I/O采用组合的方式代替继承层次太深而导致的复杂性。基于缓存区的BufferedInputStream和支持按照基本数据类型来读取的DataInputStream实现InputStream,
public class BufferedInputStream extends InputStream {
protected volatile InputStream in;
protected BufferedInputStream(InputStream in) {
this.in = in;
}
}
public class DataInputStream extends InputStream {
protected volatile InputStream in;
protected DataInputStream(InputStream in) {
this.in = in;
}
}
装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
我们发现装饰器模式和静态代理模式的实现基本相同,静态代理的实现是原始类和代理类实现相同的接口,代理类持有原始类,通过代理类实现对原始类的访问控制的功能增强。装饰器模式中的装饰器类和原始类也是继承相同的接口或抽象类,装饰器类持有原始类,通过装饰器类实现对原始类的功能增强。如果单纯从代码实现来看,静态代理和装饰器模式确实没有区别,但我们区别不同的设计模式不能单纯从代码实现来看,而是要看实现意图,装饰器模式主要的作用是给原始类添加增强功能,而代理模式通过是对原始类进行非功能性需求的扩展,如日志记录,事务等。
综上所述,装饰器模式通过组合来替代继承,解决继承关系过于复杂的问题,它主要的作用是对原始类进行功能增强。这是判断该用装饰器模式还是代理模式的重要判断依据。除此之外,装饰器模式可以对原始类嵌套使用多个装饰器,为了满足这个应用场景,装饰器类需要跟原始类继承相同的抽象类或接口。