装饰器模式的原理
装饰器模式能够实现为对象动态的添加装饰功能,它是从一个对象的外部来给对象添加功能,所以有非常灵活的扩展性,我们可以在对原来的代码毫无修改的前提下,为对象添加新功能。除此之外,装饰器模式还能够实现对象的动态组合,借此我们可以很灵活地给动态组合的对象,匹配所需要的功能。
装饰器模式包括了以下几个角色:接口、具体对象、装饰类、具体装饰类。接口定义了具体对象的一些实现方法;具体对象定义了一些初始化操作;装饰类则是一个抽象类,主要用来初始化具体对象的一个类;其它的具体装饰类都继承了该抽象类。
装饰者和被装饰对象必须有相同的父类,你可以用一个或多个装饰者包装一个对象。既然装饰者和被装饰对象有相同的父类,所以在任何需要原始对象(被包装的)的地方,我们都可以用装饰过的对象代替它。对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你需要的装饰者来装饰对象。
装饰器模式使用案例
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();
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
// Use backing collection version
i.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<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
acquireLock(key);
Object value = delegate.getObject(key);
if (value != null) {
releaseLock(key);
}
return value;
}
@Override
public Object removeObject(Object key) {
// despite of its name, this method is called only to release locks
releaseLock(key);
return null;
}
@Override
public void clear() {
delegate.clear();
}
// ...
}
之所以 MyBatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸。