Skip to content

Latest commit

 

History

History
132 lines (97 loc) · 5.43 KB

《设计模式之美》之装饰器模式.md

File metadata and controls

132 lines (97 loc) · 5.43 KB

《设计模式之美》之装饰器模式

Decorator Pattern,装饰器模式

先由Java的IO系列类入手,简单分析下使用装饰器模式的原因

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}
  • 如果要读取一个文件的内容,同时使用具备缓存功能的话,代码如上所示
  • 对于很多初学者,可能有这样的疑惑:这种使用方式有点麻烦,为什么不直接有一个具有文件读取和数据缓存功能的类呢(比如FileBufferInputStream)
  • 如果读取操作只有文件读取和数据缓存的功能的话,显然这样的设计没啥问题,并不复杂
  • 但在读取操作中,功能远比上面要多,比如有一个类叫做DataInputStream,允许按照基本数据类型来读取数据
  • 那此时如果既想对文件进行读取又想按照基本数据类型来读取,是不是就得有个FileDataInputStream了
  • 如果想把文件读取、读取基本数据类型、数据缓存功能都用上,是不是还得创建一个FileDataBufferedInputStream了
  • 每增加一个比较独立的功能,则意味着增加了一个维度,可能就不得不出现多种组合的类
  • 而且为了复用逻辑,这些新创建的类可能都是基于继承设计出来的,这么多类组成的继承关系是很复杂的,代码很不容易维护

基于装饰器模式的设计方案

先看一下Java IO中装饰器模式的体现

注意:如下代码并非Java IO中的真实实现,下文会讲到

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;
  }
}

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

我们再贴上使用代码

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();
  • 此处BufferedInputStream和DataInputStream就是装饰器类,它们负责对原始类进行功能加成;当然,FileInputStream当然就是原始类了
  • 是什么设计技巧让这种设计模式既能做到各种增加功能进行随意组合,同时又不会产生复杂的继承关系和过多的类

关键点在于

  1. 装饰器类充分利用了组合关系,比如上面的代码中,din持有bin,bin持有in,任何一个装饰器对象都不会丢。这种多级的组合持有,使得多个装饰器类无需通过继承关系就能拥有多个装饰器的功能
  2. 装饰器和原始类都遵循相同的协议,上述代码中装饰器类和原始类都继承了同一个类。这样的好处是多个装饰器类可以随意的嵌套,或者说可以无限的添加装饰器,增强原始类的能力。当然不是继承同一个类,如果是实现了同一个接口也是没有问题的

我们针对上面的使用代码,推导一下代码调用逻辑

  1. din.readInt()执行时,DataInputStream中的readInt方法中会有跟基本数据类型读取相关的增强功能逻辑
  2. 执行完增强功能逻辑后,调用this.in.read()方法,此时调用栈来到bin(即BufferedInputStream对象)中,执行具有缓存功能的读取操作
  3. 猜测一下BufferedInputStream中read方法的实现,大概率也是调用this.in.read(),其实就是执行了FileInputStream的read方法了

进一步优化

上面设计的装饰器模式代码其实还有进一步优化空间

  • 因为InputStream是一个抽象类,该类中的很多public方法(如read)其实都是有默认实现的
  • 为了能够利用强大的组合关系,让方法调用在各个装饰器中执行,并最终到达原始类中。每个装饰器类中哪怕不需要重新实现InputStream中的某个public方法,但也必须在重新写一遍。(这句话很关键,你细品)
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();
    // 如果不重新实现f()方法,则默认的实现等价于super.f(),这就无法利用组合关系,调用其他装饰器的逻辑了
  }  
}

基于上面的问题,为了不让每个装饰器编写意义不大的冗余代码。真正Java IO中的实现引入了一个FilterInputStream类

  • 它做了上面f()的工作,使得组合关系得以更容易的生效
  • 所有装饰器类继承自FilterInputStream即可