灵活又强大的装饰器模式实例与应用源码解析
保留所有版权,请引用而不是转载本文(原文地址 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.");
运行结果如图所示。
本示例中,我们使用装饰器模式对被包装类的功能进行了扩展,但是不影响原有类。遵照这个思想,还可以通过包装类增加新的方法、属性等。例如,我们给原来的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<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),欢迎关注。