装饰器模式,又称为包装模式,是一种结构型模式。这种设计模式是指能够在一个类的基础上增加一个装饰类(也可以叫包装类),并在装饰类中增加一些新的特性和功能。这样,通过对原有类的包装,就可以在不改变原有类的情况下为原有类增加更多的功能。
例如我们定义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.");
运行结果如图所示。
本示例中,我们使用装饰器模式对被包装类的功能进行了扩展,但是不影响原有类。遵照这个思想,还可以通过包装类增加新的方法、属性等。例如,我们给原来的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接口的子类中,只有一个实现类,但却有十个装饰器类。通过使用不同的装饰器装饰实现类可以让实现类有着不同的功能。
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子包中存在许多装饰器,根据装饰器的功能可以将它们可以分为以下几个大类:
- 同步装饰器:为缓存增加同步功能,如SynchronizedCache类。
- 日志装饰器:为缓存增加日志功能,如LoggingCache类。
- 清理策略装饰器:为缓存中的数据增加清理功能,如FifoCache类、LruCache类、WeakCache类、SoftCache类。
- 阻塞装饰器:为缓存增加阻塞功能,如BlockingCache类。
- 刷新装饰器:为缓存增加定时刷新功能,如ScheduledCache类。
- 序列化装饰器:为缓存增加序列化功能,如SerializedCache类。
- 事务装饰器:用于支持事务操作的装饰器,如TransactionalCache类。
《通用源码阅读指导书——MyBatis源码详解》一书中对于上述装饰器的实现进行了详细的介绍,我们不再一一展开。仅拿出做简要介绍。
FifoCache类是一个装饰器,经过它装饰的缓存会采用先进先出的策略来清理缓存,它内部使用了keyList属性存储了缓存数据的写入顺序,并且使用size属性存储了缓存数据的数量限制。当缓存中的数据达到限制时,FifoCache装饰器会将最先放入缓存中的数据删除。下面展示了FifoCache类的属性。
// 被装饰对象private final Cache delegate;// 按照写入顺序保存了缓存数据的键private final Deque keyList;// 缓存空间的大小private int size;
当向缓存中存入数据时,FifoCache类会判断数据数量是否已经超过限制。如果超过,则会将最先写入缓存的数据删除,下面展示了相关操作的源码。
/** * 向缓存写入一条数据 * @param key 数据的键 * @param value 数据的值 */@Overridepublic 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的各种缓存装饰器的实现源码都进行了介绍,看完就觉着,用好装饰器真是——灵活又强大!
最后,我是高级软件架构师易哥。
欢迎关注我,我会偶尔分享软件架构和编程方面的知识。