装饰器模式的原理

装饰器模式能够实现为对象动态的添加装饰功能,它是从一个对象的外部来给对象添加功能,所以有非常灵活的扩展性,我们可以在对原来的代码毫无修改的前提下,为对象添加新功能。除此之外,装饰器模式还能够实现对象的动态组合,借此我们可以很灵活地给动态组合的对象,匹配所需要的功能。

装饰器模式包括了以下几个角色:接口、具体对象、装饰类、具体装饰类。接口定义了具体对象的一些实现方法;具体对象定义了一些初始化操作;装饰类则是一个抽象类,主要用来初始化具体对象的一个类;其它的具体装饰类都继承了该抽象类。
image.png
装饰者和被装饰对象必须有相同的父类,你可以用一个或多个装饰者包装一个对象。既然装饰者和被装饰对象有相同的父类,所以在任何需要原始对象(被包装的)的地方,我们都可以用装饰过的对象代替它。对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你需要的装饰者来装饰对象。

装饰器模式使用案例

1. Java IO

Java IO 类库非常庞大和复杂,有几十个类,负责 IO 数据的读取和写入。针对不同的读取和写入场景,Java IO 又在这四个父类基础之上,扩展出了很多子类。具体如下所示:
image.png
其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率。典型的使用场景如下:

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

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

为什么不用继承?
如果 InputStream 只有一个子类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计一个孙子类 BufferedFileInputStream 也是可以接受的,毕竟继承结构还算简单。但实际上,继承 InputStream 的子类有很多,我们就需要给每一个 InputStream 的子类,再继续派生支持缓存读取的子类。

除了支持缓存读取外,如果还需要对功能进行其他方面的增强,比如 DataInputStream 类,支持按照基本数据类型(int、boolean 等)来读取数据。那在这种情况下,如果继续按照继承的方式来实现的话,就需要再继续派生出 N 多个子类。如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。

基于装饰器模式的方案
针对继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。下面的代码展示了 Java IO 的这种设计思路。

  1. public abstract class InputStream {
  2. //...
  3. }
  4. public class BufferedInputStream extends InputStream {
  5. // 被装饰对象
  6. protected volatile InputStream in;
  7. protected BufferedInputStream(InputStream in) {
  8. this.in = in;
  9. }
  10. //...实现基于缓存的读数据接口...
  11. }
  12. public class DataInputStream extends InputStream {
  13. // 被装饰对象
  14. protected volatile InputStream in;
  15. protected DataInputStream(InputStream in) {
  16. this.in = in;
  17. }
  18. //...实现读取基本类型数据的接口
  19. }

但装饰器模式并不是简单的“用组合替代继承”,装饰器模式相对于简单的组合关系,有两个比较特殊的点:

  • 第一点就是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。比如,下面这段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。
  1. InputStream in = new FileInputStream("/user/test.txt");
  2. InputStream bin = new BufferedInputStream(in);
  3. DataInputStream din = new DataInputStream(bin);
  4. int data = din.readInt();
  • 第二点就是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。实际上,符合“组合关系”这种代码结构的设计模式有很多,比如代理模式、桥接模式等。尽管它们的代码结构相似,但每种设计模式的意图是不同的。就拿比较相似的代理模式和装饰器模式来说:代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

实际上,在 JDK 源码的实现中,BufferedInputStream、DataInputStream 并非继承自 InputStream,而是另外一个叫 FilterInputStream 的类。那这是出于什么样的设计意图,才引入这样一个类呢?

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

对于即便是不需要增加缓存功能的函数来说,BufferedInputStream 还是必须把它重新实现一遍,简单包裹对 InputStream 对象的函数调用。具体的代码示例如下所示。因为如果不重新实现,那 BufferedInputStream 类就无法将最终读取数据的任务,委托给传递进来的 InputStream 对象来完成。

  1. public class BufferedInputStream extends InputStream {
  2. protected volatile InputStream in;
  3. protected BufferedInputStream(InputStream in) {
  4. this.in = in;
  5. }
  6. // f()函数不需要增强,只是重新调用一下InputStream对象的f()
  7. public void f() {
  8. in.f();
  9. }
  10. }

实际上,DataInputStream 也存在跟 BufferedInputStream 同样的问题。为了避免代码重复,Java IO 又抽象出了一个装饰器父类 FilterInputStream,代码实现如下。InputStream 的所有的装饰器类(BufferedInputStream、DataInputStream)都继承自这个装饰器父类。这样,装饰器类只需要实现它需要增强的方法就可以了,其他方法继承装饰器父类的默认实现。

  1. public class FilterInputStream extends InputStream {
  2. protected volatile InputStream in;
  3. protected FilterInputStream(InputStream in) {
  4. this.in = in;
  5. }
  6. public int read() throws IOException {
  7. return in.read();
  8. }
  9. public int read(byte b[]) throws IOException {
  10. return read(b, 0, b.length);
  11. }
  12. //...
  13. }

2. Java Collections 类

实际上,Java 的 Collections 类也用到了装饰器模式。Collections 类是 JDK 提供的一个集合容器工具类,提供了很多静态方法,用来创建各种集合容器,比如通过 unmodifiableColletion() 静态方法来创建 UnmodifiableCollection 类对象。而这些容器类中的 UnmodifiableCollection 类、CheckedCollection 和 SynchronizedCollection 类,就是针对 Collection 类的装饰器类。下面,我们以其中的 UnmodifiableCollection 类来举例讲解一下。其中,UnmodifiableCollection 类是 Collections 类的一个内部类,相关代码如下:

  1. public class Collections {
  2. public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) {
  3. return new UnmodifiableCollection<>(c);
  4. }
  5. static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
  6. private static final long serialVersionUID = 1820017752578914078L;
  7. final Collection<? extends E> c;
  8. UnmodifiableCollection(Collection<? extends E> c) {
  9. if (c==null)
  10. throw new NullPointerException();
  11. this.c = c;
  12. }
  13. public int size() {return c.size();}
  14. public boolean isEmpty() {return c.isEmpty();}
  15. public boolean contains(Object o) {return c.contains(o);}
  16. public Object[] toArray() {return c.toArray();}
  17. public <T> T[] toArray(T[] a) {return c.toArray(a);}
  18. public String toString() {return c.toString();}
  19. public Iterator<E> iterator() {
  20. return new Iterator<E>() {
  21. private final Iterator<? extends E> i = c.iterator();
  22. public boolean hasNext() {return i.hasNext();}
  23. public E next() {return i.next();}
  24. public void remove() {
  25. throw new UnsupportedOperationException();
  26. }
  27. @Override
  28. public void forEachRemaining(Consumer<? super E> action) {
  29. // Use backing collection version
  30. i.forEachRemaining(action);
  31. }
  32. };
  33. }
  34. public boolean add(E e) {
  35. throw new UnsupportedOperationException();
  36. }
  37. public boolean remove(Object o) {
  38. hrow new UnsupportedOperationException();
  39. }
  40. public boolean containsAll(Collection<?> coll) {
  41. return c.containsAll(coll);
  42. }
  43. public boolean addAll(Collection<? extends E> coll) {
  44. throw new UnsupportedOperationException();
  45. }
  46. public boolean removeAll(Collection<?> coll) {
  47. throw new UnsupportedOperationException();
  48. }
  49. public boolean retainAll(Collection<?> coll) {
  50. throw new UnsupportedOperationException();
  51. }
  52. public void clear() {
  53. throw new UnsupportedOperationException();
  54. }
  55. // ...
  56. }
  57. }

为什么说 UnmodifiableCollection 类是 Collection 类的装饰器类呢?这两者之间可以看作简单的接口实现关系或者类继承关系吗?

装饰器模式中的装饰器类是对原始类功能的增强。尽管 UnmodifiableCollection 类可以算是对 Collection 类的一种功能增强,但这点还不具备足够的说服力来断定 UnmodifiableCollection 就是 Collection 类的装饰器类。实际上,最关键的一点是,UnmodifiableCollection 的构造函数接收一个 Collection 类对象,然后对其所有的函数进行了包裹(Wrap):重新实现(比如 add() 函数)或者简单封装(比如 stream() 函数)。而简单的接口实现或者继承,并不会如此来实现 UnmodifiableCollection 类。所以,从代码实现的角度来说,UnmodifiableCollection 类是典型的装饰器类。

3. MyBatis-Cache

MyBatis 框架不只是简单地完成了对象和数据库数据之间的互相转化,还提供了很多其他功能,比如一级缓存和二级缓存,其缓存接口如下:

  1. public interface Cache {
  2. String getId();
  3. void putObject(Object key, Object value);
  4. Object getObject(Object key);
  5. Object removeObject(Object key);
  6. void clear();
  7. int getSize();
  8. }

MyBatis Cache 的默认实现底层采用的是一个 HashMap,只提供最基本的读写。外部可以通过对默认 Cache 的包装,来增强 Cache 的能力。MyBatis 提供了多个对 Cache 的增强类,包括:BlockingCache、FifoCache、LruCache 等。通过在增强类内部持有一个被包装的 Cache 实例,重写其 Cache 方法,在方法中加入增强逻辑,以实现原有 Cache 的增强。

以 MyBatis 提供的 BlockingCache 装饰类为例,该装饰类额外提供了阻塞获取元素的能力:

  1. public class BlockingCache implements Cache {
  2. private long timeout;
  3. // 被包装对象
  4. private final Cache delegate;
  5. private final ConcurrentHashMap<Object, ReentrantLock> locks;
  6. public BlockingCache(Cache delegate) {
  7. this.delegate = delegate;
  8. this.locks = new ConcurrentHashMap<>();
  9. }
  10. @Override
  11. public String getId() {
  12. return delegate.getId();
  13. }
  14. @Override
  15. public int getSize() {
  16. return delegate.getSize();
  17. }
  18. @Override
  19. public void putObject(Object key, Object value) {
  20. try {
  21. delegate.putObject(key, value);
  22. } finally {
  23. releaseLock(key);
  24. }
  25. }
  26. @Override
  27. public Object getObject(Object key) {
  28. acquireLock(key);
  29. Object value = delegate.getObject(key);
  30. if (value != null) {
  31. releaseLock(key);
  32. }
  33. return value;
  34. }
  35. @Override
  36. public Object removeObject(Object key) {
  37. // despite of its name, this method is called only to release locks
  38. releaseLock(key);
  39. return null;
  40. }
  41. @Override
  42. public void clear() {
  43. delegate.clear();
  44. }
  45. // ...
  46. }

之所以 MyBatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸。