装饰器模式

  • 解决问题
    • 装饰器有很多种,可以一次用一个或者多个,继承方式会使得类数目爆炸。
  • 角色
    • 具体组件 + 装饰器类
  • 经典例子
    • Java IO 库,各种输入输出流

1 解决的问题

当前招聘信息的发布,就可以抽象为一个装饰器模式。对于笔试部分,一般为了让面试者确切收到笔试消息,会通过多种途径进行提示,微信公众号平台、短信、邮箱等等。如果将这三种发送方式进行组合的话,就是 2^(3)-1=7 种组合,即 7 种要创建的发送信息的对象。

之前,如果没有较好设计的话,就涉及 7 种组合,分别发送。那么更好的设计是,将发送功能抽取出来,作为一个接口。微信公众号平台、短信、邮箱等去实现该接口的发送功能。之后再创建一个装饰器类,根据可行的发送方式,将对应的实现类注入到装饰器类的成员变量中。装饰器类同样实现发送方法,该发送方法调用其成员变量的发送方法。

这样就可以使得需要创建的类大幅下降。

除此之外,还有新增功能的装饰器模式。

通过继承的方式来增强类深度角度的特性,太复杂臃肿。如下图中,想实现 SMS、Wechat 、QQ 中一个或多个消息通知发生,通过继承来实现,类太多了。

  1. 希望增强原有的特性
  2. 希望添加新的特性

2 装饰器模式

允许动态地向一个现有的对象添加新功能或增强已有功能,同时不改变其结构,相当于对现有对象的进行了一个包装。

四个角色

首先,分为两大部分,分别是组件和装饰。组件就是平时开发时的各种对象,包含抽象组件和具体组件。装饰就是对组件进行扩展,包含抽象装饰和具体装饰。

  • Component (抽象组件) → 需要被改进的对象 & 客户端要使用的对象,内部声明需要子类实现的业务方法。
  • ConcreteComponent (具体组件) → 即具体的应用对象、抽象组件的具体实现。
  • Decorator (抽象装饰类) → 继承/实现抽象组件,重写抽象组件的方法来增强已有功能;内部包含抽象组件对象。
  • ConcreteDecorator (具体装饰类) → 抽象装饰类的具体实现,除了重写方法外,还可以添加附加功能;

3 实现代码

3.1 增强原有的功能

// 抽象组件(接口和抽象类都可以)
interface IShape {
    String show();
}
 
// 抽象装饰类(内部有一个指向组件对象的引用,用来调装饰前对象的方法)
public abstract class BaseDecorator implements IShape {
    private IShape shape;
    public BaseDecorator(IShape shape) { this.shape = shape; }
    @Override public String show() { return shape.show(); }
}
 
// 具体组件类
public class CircleShape implements IShape {
    @Override public String show() { return "圆形"; }
}
 
public class SquareShape implements IShape {
    @Override public String show() { return "矩形"; }
}
 
// 颜色具体装饰类(可调用抽象装饰类中定义的方法,也可新增方法来扩展对象行为)
public class RedDecorator extends BaseDecorator {
    public RedDecorator(IShape shape) { super(shape); }
    @Override public String show() { return "红色" + super.show(); }
}
 
public class BlueDecorator extends BaseDecorator {
    public BlueDecorator(IShape shape) { super(shape); }
    @Override public String show() { return "蓝色" + super.show(); }
}
 
// 材质具体装饰类
public class SmoothDecorator extends BaseDecorator {
    public SmoothDecorator(IShape shape) { super(shape); }
    @Override public String show() { return "光滑" + super.show(); }
}
 
public class MatteDecorator extends BaseDecorator {
    public MatteDecorator(IShape shape) { super(shape); }
    @Override public String show() { return "磨砂" + super.show(); }
}
 
// 大小具体装饰类
public class BigDecorator extends BaseDecorator {
    public BigDecorator(IShape shape) { super(shape); }
    @Override public String show() { return "大" + super.show(); }
}
 
public class MiddleDecorator extends BaseDecorator {
    public MiddleDecorator(IShape shape) { super(shape); }
    @Override public String show() { return "中" + super.show(); }
}
 
public class SmallDecorator extends BaseDecorator {
    public SmallDecorator(IShape shape) { super(shape); }
    @Override public String show() { return "小" + super.show(); }
}
 
// 测试用例
public class DecoratorTest {
    public static void main(String[] args) {
        IShape circle = new CircleShape();
        IShape square = new SquareShape();
        IShape redCircle =  new RedDecorator(circle);
        IShape smoothBlueSquare = new SmoothDecorator(new BlueDecorator(square));
        IShape bigMatteRedCircle = new BigDecorator(new MatteDecorator(redCircle));
        System.out.println(circle.show());
        System.out.println(square.show());
        System.out.println(redCircle.show());
        System.out.println(smoothBlueSquare.show());
        System.out.println(bigMatteRedCircle.show());
    }
}
 

用继承要 24 个子类,用装饰器模式只要 10 个,顺带画出 UML 类图:

3.2 用于添加功能的装饰模式

我们用程序来模拟一下房屋装饰粘钩后,新增了挂东西功能的过程:

新建房屋接口:

public interface IHouse {
    void live();
}

房屋类:

public class House implements IHouse{
    @Override
    public void live() {
        System.out.println("房屋原有的功能:居住功能");
    }
}

新建粘钩装饰器接口,继承自房屋接口:

public interface IStickyHookHouse extends IHouse{
    void hangThings();
}

粘钩装饰类:(这里被装饰者 IHouse 是透明的)

public class StickyHookDecorator implements IStickyHookHouse {
    private final IHouse house;
	public StickyHookDecorator(IHouse house) {        this.house = house;    }
	@Override    public void live() {        house.live();    }
	@Override    public void hangThings() {        System.out.println("有了粘钩后,新增了挂东西功能");    }
}

客户端测试:

public class Client {
    @Test
    public void show() {
        IHouse house = new House();
        house.live();
        IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
        stickyHookHouse.live();
        stickyHookHouse.hangThings();
    }
}

运行程序,显示如下:

房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了粘钩后,新增了挂东西功能

没有修改原有的功能,只是扩展了新的功能,这种模式在装饰模式中称之为 半透明装饰模式

  • 因为装饰后的接口具有原接口不具有的方法,因此客户端使用时要区别对待,新接口是可见的、不透明的。
  • 因为被装饰者可以是实现了原接口的任意对象,所以被装饰者对客户端是不可见的、透明的。

半透明装饰模式中我们无法多次装饰,因为将添加新功能后的类 A 再次传入其他装饰类 B 中时,B 类是并不认识 A 类中添加的新功能,所以无法进行多次装饰。

只要添加了新功能的装饰模式都称之为 半透明装饰模式,他们都具有不可以多次装饰的特点。既增强了功能,又添加了新功能的装饰模式仍然具有半透明特性。

4 使用场景

  • 在不影响其他对象的情况下,快速动态透明地为单个对象添加功能;
  • 动态地增强对象的特性 or 添加功能,链式地;
  • 不支持继承扩展类的场景,如 final 关键字修饰的类;

5 优缺点

优点

  • 快速扩展对象功能,比继承灵活,不会导致类个数急剧增加,动态增删对象实例功能,按需按顺序组合功能;

缺点

  • 顺序调用链装饰时,删除某个装饰器需要修改上下文代码;
  • 容易增加很多装饰对象,增加代理理解难度;
  • 和桥接模式一样,组合相比继承,更不容易找到对象间的调用关系;

6 经典应用例子

6.1 Java IO 类库

IO 读入的方法与装饰器模式很像,其实背后也用到了装饰器模式。

InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt"));

InputStream 的继承关系如下:

这个图和上面例子中的图大致可以对应上。其中,InputStream 是一个抽象类,对应上文例子中的 IShape,其中最重要的方法是 read 方法,这是一个抽象方法:

public abstract class InputStream implements Closeable {
    public abstract int read() throws IOException;
    // ...
}

上图中,左边的三个类 FileInputStreamByteArrayInputStreamServletInputStreamInputStream 的三个子类,对应上文例子中实现了 IShape 接口的各种形状。

右下角的三个类 BufferedInputStreamDataInputStreamCheckedInputStream 是三个具体的装饰者类,他们都为 InputStream 增强了原有功能或添加了新功能。

FilterInputStream 是所有装饰类的父类,它没有实现具体的功能,仅用来包装了一下 InputStream:

public class FilterInputStream extends InputStream {
    protected volatile InputStream in;
    protected FilterInputStream(InputStream in) {        this.in = in;    }
    public int read() throws IOException {        return in.read();    }
 
    //...
}

这里生成装饰类的父类 FilterInputS tream 的原因在于:
有些装饰器本身不需要真正处理 read()等方法,但是装饰器模式的 链式传递,不用到也要实现这些方法。而每个这样的装饰器都重写方法的话,会存在大量重复代码。用一个装饰器父类 FilterInputStream 提供默认实现,以此减少这些重复代码。

我们以 BufferedInputStream 为例。原有的 InputStream 读取文件时,是一个字节一个字节读取的,这种方式的执行效率并不高,所以我们可以设立一个缓冲区,先将内容读取到缓冲区中,缓冲区读满后,将内容从缓冲区中取出来,这样就变成了一段一段读取,用内存换取效率。BufferedInputStream 就是用来做这个的。它继承自 FilterInputStream

public class BufferedInputStream extends FilterInputStream {
    private static final int DEFAULT_BUFFER_SIZE = 8192;
    protected volatile byte buf[];

    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
 
    //...
}

我们先来看它的构造方法,在构造方法中,新建了一个 byte[] 作为缓冲区,从源码中我们看到,Java 默认设置的缓冲区大小为 8192 byte,也就是 8 KB。

然后我们来查看 read 方法:

public class BufferedInputStream extends FilterInputStream {
    //...
    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }
    private void fill() throws IOException {
        // 往缓冲区内填充读取内容的过程
        //...
    }
}

在 read 方法中,调用了 fill 方法,fill 方法的作用就是往缓冲区中填充读取的内容。这样就实现了增强原有的功能。

在源码中我们发现,BufferedInputStream 没有添加 InputStream 中没有的方法,所以 BufferedInputStream 使用的是 透明的装饰模式

DataInputStream 用于更加方便地读取 int、double 等内容,观察 DataInputStream 的源码可以发现,DataInputStream 中新增了 readInt、readLong 等方法,所以 DataInputStream 使用的是 半透明装饰模式