装饰器模式的原理
装饰器模式能够实现为对象动态的添加装饰功能,它是从一个对象的外部来给对象添加功能,所以有非常灵活的扩展性,我们可以在对原来的代码毫无修改的前提下,为对象添加新功能。除此之外,装饰器模式还能够实现对象的动态组合,借此我们可以很灵活地给动态组合的对象,匹配所需要的功能。
装饰器模式包括了以下几个角色:接口、具体对象、装饰类、具体装饰类。接口定义了具体对象的一些实现方法;具体对象定义了一些初始化操作;装饰类则是一个抽象类,主要用来初始化具体对象的一个类;其它的具体装饰类都继承了该抽象类。
装饰者和被装饰对象必须有相同的父类,你可以用一个或多个装饰者包装一个对象。既然装饰者和被装饰对象有相同的父类,所以在任何需要原始对象(被包装的)的地方,我们都可以用装饰过的对象代替它。对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你需要的装饰者来装饰对象。
装饰器模式使用案例
1. Java IO
Java IO 类库非常庞大和复杂,有几十个类,负责 IO 数据的读取和写入。针对不同的读取和写入场景,Java IO 又在这四个父类基础之上,扩展出了很多子类。具体如下所示:
其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率。典型的使用场景如下:
InputStream in = new FileInputStream("/user/test.txt");InputStream bin = new BufferedInputStream(in);byte[] data = new byte[128];while (bin.read(data) != -1) {//...}
这样的用法比较麻烦。那 Java IO 为什么不设计一个继承 FileInputStream 并且支持缓存的 BufferedFileInputStream 类呢?这样我们就可以直接创建一个 BufferedFileInputStream 类对象,打开文件读取数据,用起来岂不是更加简单?
为什么不用继承?
如果 InputStream 只有一个子类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计一个孙子类 BufferedFileInputStream 也是可以接受的,毕竟继承结构还算简单。但实际上,继承 InputStream 的子类有很多,我们就需要给每一个 InputStream 的子类,再继续派生支持缓存读取的子类。
除了支持缓存读取外,如果还需要对功能进行其他方面的增强,比如 DataInputStream 类,支持按照基本数据类型(int、boolean 等)来读取数据。那在这种情况下,如果继续按照继承的方式来实现的话,就需要再继续派生出 N 多个子类。如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。
基于装饰器模式的方案
针对继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。下面的代码展示了 Java IO 的这种设计思路。
public abstract class 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;}//...实现读取基本类型数据的接口}
但装饰器模式并不是简单的“用组合替代继承”,装饰器模式相对于简单的组合关系,有两个比较特殊的点:
- 第一点就是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。比如,下面这段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。
InputStream in = new FileInputStream("/user/test.txt");InputStream bin = new BufferedInputStream(in);DataInputStream din = new DataInputStream(bin);int data = din.readInt();
- 第二点就是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。实际上,符合“组合关系”这种代码结构的设计模式有很多,比如代理模式、桥接模式等。尽管它们的代码结构相似,但每种设计模式的意图是不同的。就拿比较相似的代理模式和装饰器模式来说:代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
实际上,在 JDK 源码的实现中,BufferedInputStream、DataInputStream 并非继承自 InputStream,而是另外一个叫 FilterInputStream 的类。那这是出于什么样的设计意图,才引入这样一个类呢?
我们知道,InputStream 是一个抽象类而非接口,而且它的大部分函数(比如 read、available)都有默认实现,按理来说,我们只需要在 BufferedInputStream 类中重新实现那些需要增加缓存功能的函数就可以了,其他函数继承 InputStream 的默认实现。但实际上,这样做是行不通的。
对于即便是不需要增加缓存功能的函数来说,BufferedInputStream 还是必须把它重新实现一遍,简单包裹对 InputStream 对象的函数调用。具体的代码示例如下所示。因为如果不重新实现,那 BufferedInputStream 类就无法将最终读取数据的任务,委托给传递进来的 InputStream 对象来完成。
public class BufferedInputStream extends InputStream {protected volatile InputStream in;protected BufferedInputStream(InputStream in) {this.in = in;}// f()函数不需要增强,只是重新调用一下InputStream对象的f()public void f() {in.f();}}
实际上,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);}//...}
2. Java Collections 类
实际上,Java 的 Collections 类也用到了装饰器模式。Collections 类是 JDK 提供的一个集合容器工具类,提供了很多静态方法,用来创建各种集合容器,比如通过 unmodifiableColletion() 静态方法来创建 UnmodifiableCollection 类对象。而这些容器类中的 UnmodifiableCollection 类、CheckedCollection 和 SynchronizedCollection 类,就是针对 Collection 类的装饰器类。下面,我们以其中的 UnmodifiableCollection 类来举例讲解一下。其中,UnmodifiableCollection 类是 Collections 类的一个内部类,相关代码如下:
public class Collections {public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) {return new UnmodifiableCollection<>(c);}static class UnmodifiableCollection<E> implements Collection<E>, Serializable {private static final long serialVersionUID = 1820017752578914078L;final Collection<? extends E> c;UnmodifiableCollection(Collection<? extends E> c) {if (c==null)throw new NullPointerException();this.c = c;}public int size() {return c.size();}public boolean isEmpty() {return c.isEmpty();}public boolean contains(Object o) {return c.contains(o);}public Object[] toArray() {return c.toArray();}public <T> T[] toArray(T[] a) {return c.toArray(a);}public String toString() {return c.toString();}public Iterator<E> iterator() {return new Iterator<E>() {private final Iterator<? extends E> i = c.iterator();public boolean hasNext() {return i.hasNext();}public E next() {return i.next();}public void remove() {throw new UnsupportedOperationException();}@Overridepublic void forEachRemaining(Consumer<? super E> action) {// Use backing collection versioni.forEachRemaining(action);}};}public boolean add(E e) {throw new UnsupportedOperationException();}public boolean remove(Object o) {hrow new UnsupportedOperationException();}public boolean containsAll(Collection<?> coll) {return c.containsAll(coll);}public boolean addAll(Collection<? extends E> coll) {throw new UnsupportedOperationException();}public boolean removeAll(Collection<?> coll) {throw new UnsupportedOperationException();}public boolean retainAll(Collection<?> coll) {throw new UnsupportedOperationException();}public void clear() {throw new UnsupportedOperationException();}// ...}}
为什么说 UnmodifiableCollection 类是 Collection 类的装饰器类呢?这两者之间可以看作简单的接口实现关系或者类继承关系吗?
装饰器模式中的装饰器类是对原始类功能的增强。尽管 UnmodifiableCollection 类可以算是对 Collection 类的一种功能增强,但这点还不具备足够的说服力来断定 UnmodifiableCollection 就是 Collection 类的装饰器类。实际上,最关键的一点是,UnmodifiableCollection 的构造函数接收一个 Collection 类对象,然后对其所有的函数进行了包裹(Wrap):重新实现(比如 add() 函数)或者简单封装(比如 stream() 函数)。而简单的接口实现或者继承,并不会如此来实现 UnmodifiableCollection 类。所以,从代码实现的角度来说,UnmodifiableCollection 类是典型的装饰器类。
3. MyBatis-Cache
MyBatis 框架不只是简单地完成了对象和数据库数据之间的互相转化,还提供了很多其他功能,比如一级缓存和二级缓存,其缓存接口如下:
public interface Cache {String getId();void putObject(Object key, Object value);Object getObject(Object key);Object removeObject(Object key);void clear();int getSize();}
MyBatis Cache 的默认实现底层采用的是一个 HashMap,只提供最基本的读写。外部可以通过对默认 Cache 的包装,来增强 Cache 的能力。MyBatis 提供了多个对 Cache 的增强类,包括:BlockingCache、FifoCache、LruCache 等。通过在增强类内部持有一个被包装的 Cache 实例,重写其 Cache 方法,在方法中加入增强逻辑,以实现原有 Cache 的增强。
以 MyBatis 提供的 BlockingCache 装饰类为例,该装饰类额外提供了阻塞获取元素的能力:
public class BlockingCache implements Cache {private long timeout;// 被包装对象private final Cache delegate;private final ConcurrentHashMap<Object, ReentrantLock> locks;public BlockingCache(Cache delegate) {this.delegate = delegate;this.locks = new ConcurrentHashMap<>();}@Overridepublic String getId() {return delegate.getId();}@Overridepublic int getSize() {return delegate.getSize();}@Overridepublic void putObject(Object key, Object value) {try {delegate.putObject(key, value);} finally {releaseLock(key);}}@Overridepublic Object getObject(Object key) {acquireLock(key);Object value = delegate.getObject(key);if (value != null) {releaseLock(key);}return value;}@Overridepublic Object removeObject(Object key) {// despite of its name, this method is called only to release locksreleaseLock(key);return null;}@Overridepublic void clear() {delegate.clear();}// ...}
之所以 MyBatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸。
