跳至主要內容

装饰器模式

AruNi_Lu设计模式设计模式与范式约 2368 字大约 8 分钟

本文内容

前言

装饰器模式也是一种结构型模式,在 Java 的 IO 类中就有使用到,所以下面我们就从 Java IO 类的设计,来看看什么是装饰器模式?使用它有什么好处?它与其他的结构型模式又有什么区别?

1. Java IO 类

Java 的 IO 类库非常庞大和复杂,有十几个类,我当时在学 IO 操作时就被这些类给绕晕了。

为了让这些 IO 类更容易理解,我们可以将其划分为四大类:

类型字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

其他杂七杂八的类都是这四大类的子类(实现类),主要是为了应对不同的 IO 场景。具体关系如下:

我们一般在使用 IO 类时,都会像下面这样使用:

InputStream in = new FileInputStream("/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}
  • InputStream 是一个抽象类;
  • FileInputStream 是专门用来读取文件流的子类;
  • BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率。

可以发现,当我们 需要使用带有缓存功能的输入流时,需要先创建一个 FileInputStream 对象,然后再传递给 BufferedInputStream 对象来使用

你是否觉得这有些许麻烦呢?为啥不直接设计一个继承 FileInputStream 并且支持缓存的 BufferedFileInputStream 类来使用呢?这样就可以直接 new 一个 BufferedFileInputStream 对象来打开文件读取数据。就像下面这样:

InputStream bin = new BufferedFileInputStream("/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}

下面我们就来分析一下,Java 开发团队为什么不选择这样做的原因。

2. 基于继承的设计

如果 InputStream 只有一个子类 FileInputStream,那么在 FileInputStream 的基础上,再设计一个 BufferedFileInputStream,继承结构还算简单,可以接收。

但是,InputStream 的子类有很多,那么我们就需要为这些子类都派生一个支持缓存的子类,这样类的结构就会变得复杂起来了

除了支持缓存外,如果还需要对其他方面进行增强,比如 DataInputStream,用来支持按照基本数据类型来读取数据,如下所示:

FileInputStream in = new FileInputStream("/test.txt");
DataInputStream din = new DataInputStream(in);
int data = din.readInt();

此时如果还使用继承来实现,那么就需要再派生出更多的子类,每对一个方面进行增强,就要对所有 InputStream 的子类进行派生,这样类的继承结构便变得无比复杂,可扩展性和可维护性都受到了影响。

那么下面来看看,Java 开发团队是怎么利用装饰器模式来解决上面问题的。

3. 基于装饰器模式的设计

针对上面继承结果过于复杂的问题,我们可以 将继承关系改为组合关系 来解决,也体现了 “组合优于继承” 这一思想。

下面来看看 Java IO 的这种设计思路(简化修改版):

顶层抽象类 InputStream:

public abstract class InputStream {
  //...
  public int read(byte b[]) throws IOException {
    return read(b, 0, b.length);
  }
  
  public int read(byte b[], int off, int len) throws IOException {
    //...
  }
  
  public long skip(long n) throws IOException {
    //...
  }

  public int available() throws IOException {
    return 0;
  }
  
  public void close() throws IOException {}

  public synchronized void mark(int readlimit) {}
    
  public synchronized void reset() throws IOException {
    throw new IOException("mark/reset not supported");
  }

  public boolean markSupported() {
    return false;
  }
}

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;
  }
  
  //...实现读取基本类型数据的接口
}

从上面代码来看,装饰器模式并不只是简单的 “用组合替代继承”,还有两个比较特殊的地方。

第一个特殊的地方是:装饰器类和原始类继承统一的父类,这样可以对原始类 “嵌套” 多个装饰器类

在 Java IO 中的具体体现:原始类(FileInputStream)、两个装饰器类(BufferedInputStream 和 DataInputStream)都继承自 InputStream 这个统一的父类。那么我们就可以将装饰器类对象,传递进另一个装饰器类中,进行多个装饰器类的 “嵌套”,就像下面这样:

// 对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,
// 让它既支持缓存读取,又支持按照基本数据类型来读取数据
InputStream in = new FileInputStream("/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();

第二个特殊的地方是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点

其实符合 “组合关系” 的设计模式有很多,比如代理模式,桥接模式。尽管它们的代码结构相似,但是 每种设计模式的意图是不同的

就拿比较相似的代理模式和装饰器模式来说:

  • 代理模式中,代理类附加的是跟原始类无关的功能
  • 装饰器模式中,装饰器类附加的是跟原始类相关的增强功能

代理模式代码结构:

// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
  void f();
}

public class A impelements IA {
  public void f() { //... }
}

public class AProxy implements IA {
  private IA a;
  public AProxy(IA a) {
    this.a = a;
  }
  
  public void f() {
    // 新添加的代理逻辑
    a.f();	// 执行核心逻辑
    // 新添加的代理逻辑
  }
}

装饰器模式代码结构:

// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
  void f();
}

public class A implements IA {
  public void f() { //... }
}

public class ADecorator implements IA {
  private IA a;
  public ADecorator(IA a) {
    this.a = a;
  }
  
  public void f() {
    // 功能增强代码
    a.f();	// 执行核心逻辑
    // 功能增强代码
  }
}

好了,装饰器模式差不多讲完了,上面给出的 Java IO 的装饰器设计其实是修改过的,JDK 源码并不是那样设计的。

在 JDK 源码中,BufferdInputStream 和 DataInputStream 其实并不是继承自 InputStream,而是 继承自 FilterInputStream 类(FilterInputStream 继承自 InputStream),为什么要这么设计呢?

InputStream 是一个抽象类而非接口,而且它的大部分函数(如 read()available())都有默认实现。按理来说,我们只需要在 BufferedInputStream 类中重写那些需要增加缓存功能的函数就可以了,其他函数继承 InputStream 的默认实现即可。但实际上,这样做是 行不通 的,为什么呢?

对于即便是不需要增加缓存功能的函数来说,BufferedInputStream 还是必须把它重新实现一遍,简单包裹对 InputStream 对象的函数调用,就像下面这样:

public class BufferedInputStream extends InputStream {
  protected volatile InputStream in;

  protected BufferedInputStream(InputStream in) {
    this.in = in;
  }
  
  // f() 函数不需要增强,只是重新调用一下传进来的 InputStream in 对象的 f()
  public void f() {
    in.f();
  }  
}

如果不重新实现,那么 BufferedInputStream 类就无法将最终读取数据的任务,委托给传递进来的 InputStream 对象(InputStream 子类对象)来完成

具体来说,装饰器(如 BufferedInputStream、DataInputStream 等)本身并不真正处理 read() 等方法,而是由构造函数传入的被装饰对象 InputStream(实际上是 FileInputStream 或者ByteArrayInputStream 等对象)来完成。如果不重写默认的 read() 等方法,则无法执行如 FileInputStream 或者ByteArrayInputStream 等对象所真正实现的 read 功能

同理,DataInputStream 也存在跟 BufferedInputStream 同样的问题。为了 避免代码重复(即两个装饰器类都重新实现其它函数),所以 Java IO 抽象出了一个装饰器父类 FilterInputStream。InputStream 的所有的装饰器类(BufferedInputStream、DataInputStream)都继承自这个装饰器父类。实现如下:

public class FilterInputStream extends InputStream {
  protected volatile InputStream in;

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

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

  public int read(byte b[]) throws IOException {
    return read(b, 0, b.length);
  }
   
  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 int available() throws IOException {
    return in.available();
  }

  public void close() throws IOException {
    in.close();
  }

  public synchronized void mark(int readlimit) {
    in.mark(readlimit);
  }

  public synchronized void reset() throws IOException {
    in.reset();
  }

  public boolean markSupported() {
    return in.markSupported();
  }
}

这样,装饰器类只需要实现它需要增强的方法就可以了,其他方法继承装饰器父类的默认实现

4. 总结

通过分析 Java IO 的设计,就把装饰器模式学习完了,这样的学习方式真是一举两得。

装饰器模式通过 “组合替代继承关系”,用于 解决继承关系过于复杂的问题。它主要的作用是 给原始类添加增强功能,这也是判断是否应改用它的一个重要依据。

此外,装饰器模式还有一个特点就是 可以对原始类嵌套使用多个装饰器。为了满足这个特点,装饰器类需要跟原始类继承相同的抽象类或者接口

上次编辑于: