设计模式与范式 --- 结构型模式(装饰器模式)

简介: 设计模式与范式 --- 结构型模式(装饰器模式)

写在前



如果对 Java IO 类做一下分类,我们可以从下面两个维度将它划分为四类:


  • 输入流字节流:InputStream
  • 输出流字节流:OutputStream
  • 输入流字符流:Reader
  • 输出流字符流:Writer


针对不同的读取和写入场景,Java IO 又在这四个父类基础之上,扩展出了很多子类。


当我们打开文件 test.txt,从中读取数据。其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率。代码如下:·

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


初看上面的代码,我们会觉得 Java IO 的用法比较麻烦,需要先创建一个 FileInputStream 对象,然后再传递给 BufferedInputStream 对象来使用。


1.基于继承的设计方案



Java IO 为什么不设计一个继承 FileInputStream 并且支持缓存的 BufferedFileInputStream 类呢?这样我们就可以像下面的代码中这样,直接创建一个 BufferedFileInputStream 类对象,打开文件读取数据,用起来岂不是更加简单?

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


对于上述基于继承的设计方案,如果 InputStream 只有一个子类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计一个孙子类 BufferedFileInputStream,也算是可以接受的,毕竟继承结构还算简单。


但实际上,继承 InputStream 的子类有很多。我们需要给每一个 InputStream 的子类,再继续派生支持缓存读取的子类。


除了支持缓存读取之外,如果我们还需要对功能进行其他方面的增强,比如下面的 DataInputStream 类,支持按照基本数据类型(int、boolean、long等)来读取数据。

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


在这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出 DataFileInputStream、DataPipedInputStream 等类。


如果我们还需要既支持缓存、又支持按照基本类型读取数据的类,那就要再继续派生出 BufferedDataFileInputStream、BufferedDataPipedInputStream 等 n 多类。


这还只是附加了两个增强功能,如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。


2.装饰者模式设计方案



针对刚刚的继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。

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增强InputStream带缓存功能的数据读取能力,提高数据流的读写效率
public class BufferedInputStream extends InputStream {
  protected volatile InputStream in;
  protected BufferedInputStream(InputStream in) {
    this.in = in;
  }
  //...实现基于缓存的读数据接口...  
}
// 装饰者类:DataInputStream增强InputStream按基本数据类型来读取数据能力
public class DataInputStream extends InputStream {
  protected volatile InputStream in;
  protected DataInputStream(InputStream in) {
    this.in = in;
  }
  //...实现读取基本类型数据的接口
}


从 Java IO 的设计来看,装饰器模式相对于简单的组合关系,还有两个比较特殊的地方


  • 第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。


下面这样一段代码,我们对 FileInputStream 嵌套了两个装饰器类:


BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。

InputStream in = new FileInputStream("/user/wangzheng/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 impements IA {
  private IA a;
  public AProxy(IA a) {
    this.a = a;
  }
  public void f() {
    // 新添加的代理逻辑
    a.f();
    // 新添加的代理逻辑
  }
}
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
  void f();
}
public class A impelements IA {
  public void f() { //... }
}
public class ADecorator impements IA {
  private IA a;
  public ADecorator(IA a) {
    this.a = a;
  }
  public void f() {
    // 功能增强代码
    a.f();
    // 功能增强代码
  }
}


实际上,如果去查看JDK的源码,你会发现,BufferedInputStream、DataInputStream 并非继承自 InputStream,而是另外一个叫 FilterInputStream 的类。


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


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

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


实际上,DataInputStream 也存在跟 BufferedInputStream 同样的问题。为了避免代码重复,Java IO 抽象出了一个装饰器父类 FilterInputStream,代码实现如下所示。

// 避免装饰器类代码重复抽象出的父类
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();
  }
}


3.参考



极客时间《设计模式之美》 ----- 王争

相关文章
|
5月前
|
设计模式 存储 缓存
【设计模式】【结构型模式】享元模式(Flyweight)
一、入门 什么是享元模式? 享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来减少内存使用,特别适用于存在大量相似对象的情况。 它的核心思想是将对象的内在状态(不变
190 16
|
5月前
|
设计模式 Java 数据库连接
【设计模式】【结构型模式】代理模式(Proxy)
一、入门 什么是代理模式? 代理模式(Proxy Pattern)是一种结构型设计模式,允许你提供一个代理对象来控制对另一个对象的访问。 代理对象在客户端和目标对象之间起到中介作用,可以在不改变目标对
120 10
|
5月前
|
设计模式 Java 定位技术
【设计模式】【结构型模式】组合模式(Composite)
一、入门 什么是组合模式 组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构来表示“部分-整体”的层次关系。组合模式使得客户端可以统一处理单个对象和组合对
158 10
|
5月前
|
关系型数据库 Java MySQL
【设计模式】【结构型模式】桥接模式(Bridge)
一、入门 什么是桥接模式? 桥接模式(Bridge Pattern)是一种结构型设计模式,核心思想是将抽象与实现分离,让它们可以独立变化。简单来说,它像一座“桥”连接了两个维度的变化,避免用继承导致代
321 10
|
5月前
|
设计模式 前端开发 Java
【设计模式】【结构型模式】适配器模式(Adpter)
一、入门 什么是适配器模式? 适配器模式是Java中常用的结构型设计模式,它的核心作用就像现实中的电源转换器一样---让原本不兼容的两个接口能够协同工作。 为什么要用适配器模式? 假设我们需要在电商系
130 10
|
5月前
|
设计模式 Java 数据库连接
【设计模式】【结构型模式】外观模式(Facde)
一、入门 什么是外观模式? 一种结构型设计模式,通过为子系统中的一组接口提供一个统一的高层接口(称为外观),来简化客户端与复杂子系统的交互过程。其本质是建立抽象层来隔离复杂度。 为什么要有外观模式?
241 9
|
5月前
|
设计模式 缓存 安全
【设计模式】【结构型模式】装饰者模式(Decorator)
一、入门 什么是装饰者模式? 装饰者模式(Decorator Pattern)是 Java 中常用的结构型设计模式,它能在不修改原有对象结构的前提下,动态地为对象添加额外的职责。 为什么要装饰者模式?
107 8
|
设计模式 XML Java
【设计模式】装饰器模式(定义 | 特点 | Demo入门讲解)
【设计模式】装饰器模式(定义 | 特点 | Demo入门讲解)
108 0
|
8月前
|
设计模式 中间件 Java
设计模式3:代理、适配器、装饰器模式
代理模式是一种结构型设计模式,通过中间件解耦服务提供者和使用者,使使用者间接访问服务提供者,便于封装和控制。
135 3
|
10月前
|
设计模式 前端开发 JavaScript
前端必须掌握的设计模式——装饰器模式
装饰器模式是一种结构型设计模式,通过创建新类来包装原始对象,实现在不修改原有结构的前提下扩展新行为。其核心在于“组合”思想,使新功能可“即插即拔”。该模式具有解耦性、灵活性和动态性等特点,广泛应用于类的面向对象编程语言中,如JavaScript的注解和TypeScript的写法。示例中,通过装饰器模式为游戏角色动态添加装备,展示了其强大的扩展性和灵活性。
191 16