灵活又强大的装饰器模式实例与应用源码解析

标签: 设计模式, Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/47/ )。

装饰器模式,又称为包装模式,是一种结构型模式。这种设计模式是指能够在一个类的基础上增加一个装饰类(也可以叫包装类),并在装饰类中增加一些新的特性和功能。这样,通过对原有类的包装,就可以在不改变原有类的情况下为原有类增加更多的功能。

例如我们定义Phone接口,它规定了发送和接收语音的抽象方法。

public interface Phone {
    String callIn();
    Boolean callOut(String info);
}

然后定义一个类TelePhone,实现了Phone接口,能够实现打电话的功能。

public class TelePhone implements Phone {
    @Override
    public String callIn() {
        System.out.println("接受语音……");
        return "get info";
    }

    @Override
    public Boolean callOut(String info) {
        System.out.println("发送语音:" + info);
        return true;
    }
}

那现在我们要创建一个装饰器,在不改变原有TelePhone的基础上,实现通话录音功能。我们的装饰器类的源码如下所示。

public class PhoneRecordDecorator implements Phone {
    private Phone decoratedPhone;

    public PhoneRecordDecorator(Phone decoratedPhone) {
        this.decoratedPhone = decoratedPhone;
    }

    @Override
    public String callIn() {
        System.out.println("启动录音……");
        String info = decoratedPhone.callIn();
        System.out.println("结束录音并保存录音文件。");
        return info;
    }

    @Override
    public Boolean callOut(String info) {
        System.out.println("启动录音……");
        Boolean result = decoratedPhone.callOut(info);
        System.out.println("结束录音并保存录音文件。");
        return result;
    }
}

这样,经过我们的PhoneRecordDecorator包装过的Phone就具有了通话录音的能力,如下所示。

System.out.println("--原有Phone无录音功能--");
Phone phone = new TelePhone();
phone.callOut("Hello, this is yee.");

System.out.println();

System.out.println("--经过装饰后的Phone有录音功能--");
Phone phoneWithRecorder = new PhoneRecordDecorator(phone);
phoneWithRecorder.callOut("Hello, this is yee.");

运行结果如图所示。

图 6-1 程序运行结果

本示例中,我们使用装饰器模式对被包装类的功能进行了扩展,但是不影响原有类。遵照这个思想,还可以通过包装类增加新的方法、属性等。例如,我们给原来的TelePhone类增加收发短信功能,如下所示。

public class PhoneMessageDecorator implements Phone {
    private Phone decoratedPhone;

    public PhoneMessageDecorator(Phone decoratedPhone) {
        this.decoratedPhone = decoratedPhone;
    }

    @Override
    public String callIn() {
        return decoratedPhone.callIn();
    }

    @Override
    public Boolean callOut(String info) {
        return decoratedPhone.callOut(info);
    }

    public String receiveMessage() {
        // 省略接受短信操作
        return "receive message";
    }
    
    public Boolean sendMessage(String info) {
        // 省略发送短信操作
        return true;
    }
}

装饰器模式在编程开发中经常使用。通常的使用场景是在一个核心基本类的基础上,提供大量的装饰器,从而使得核心基本类经过不同的装饰器修饰后获得不同的功能。(均参考自《通用源码阅读指导书——MyBatis源码详解》)

装饰器还有一个优点就是可以叠加使用,即一个核心基本类可以被多个装饰器修饰,从而同时具有这多个装饰器的功能。

MyBatis便使用了大量的装饰器实现了不同类型的缓存,它们都在cache包中。在imple子包中存放了实现类,在decorators子包中存放了众多装饰器类。而Cache接口是实现类和装饰器类的共同接口。

下面给出了Cache接口及其子类的类图。Cache接口的子类中,只有一个实现类,但却有十个装饰器类。通过使用不同的装饰器装饰实现类可以让实现类有着不同的功能。

图 19-3 Cache接口及其子类的类图

Cache接口的源码如下所示,在接口中定义了实现类和装饰器类中必须实现的方法。

public interface Cache {

  /**
   * 获取缓存id
   * @return 缓存id
   */
  String getId();

  /**
   * 向缓存写入一条数据
   * @param key 数据的键
   * @param value 数据的值
   */
  void putObject(Object key, Object value);

  /**
   * 从缓存中读取一条数据
   * @param key 数据的键
   * @return 数据的值
   */
  Object getObject(Object key);

  /**
   * 从缓存中删除一条数据
   * @param key 数据的键
   * @return 原来的数据值
   */
  Object removeObject(Object key);

  /**
   * 清空缓存
   */
  void clear();

  /**
   * 读取缓存中数据的数目
   * @return 数据的数目
   */
  int getSize();

  /**
   * 获取读写锁,该方法已经废弃
   * @return 读写锁
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

缓存实现类PerpetualCache的实现非常简单,但可以通过装饰器来为其增加更多的功能。decorators子包中存在许多装饰器,根据装饰器的功能可以将它们可以分为以下几个大类:

《通用源码阅读指导书——MyBatis源码详解》一书中对于上述装饰器的实现进行了详细的介绍,我们不再一一展开。仅拿出做简要介绍。

FifoCache类是一个装饰器,经过它装饰的缓存会采用先进先出的策略来清理缓存,它内部使用了keyList属性存储了缓存数据的写入顺序,并且使用size属性存储了缓存数据的数量限制。当缓存中的数据达到限制时,FifoCache装饰器会将最先放入缓存中的数据删除。下面展示了FifoCache类的属性。

// 被装饰对象
private final Cache delegate;
// 按照写入顺序保存了缓存数据的键
private final Deque<Object> keyList;
// 缓存空间的大小
private int size;

当向缓存中存入数据时,FifoCache类会判断数据数量是否已经超过限制。如果超过,则会将最先写入缓存的数据删除,下面展示了相关操作的源码。

/**
 * 向缓存写入一条数据
 * @param key 数据的键
 * @param value 数据的值
 */
@Override
public void putObject(Object key, Object value) {
  cycleKeyList(key);
  delegate.putObject(key, value);
}

/**
 * 记录当前放入的数据的键,同时根据空间设置清除超出的数据
 * @param key 当前放入的数据的键
 */
private void cycleKeyList(Object key) {
  keyList.addLast(key);
  if (keyList.size() > size) {
    Object oldestKey = keyList.removeFirst();
    delegate.removeObject(oldestKey);
  }
}

其中的delegate引用的就是缓存最终的实现类,当然也可能是被装饰器装饰后的实现类。毕竟,我们也说过,装饰器可以叠加使用。

关于其他各种缓存装饰器的实现、装饰器的叠加使用等等,大家可以参考《通用源码阅读指导书——MyBatis源码详解》。这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用,非常推荐。

通用源码阅读指导书-京东自营

《通用源码阅读指导书》

书中对于Mybatis的各种缓存装饰器的实现源码都进行了介绍,看完就觉着,用好装饰器真是——灵活又强大!

本文首发于个人知乎:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐