前面介绍了创建型设计模式常用的模式:单例模式、工厂模式(工厂方法和抽象工厂)、建造者模式,本文聊聊原型模式,那么原型模式应该在什么场景下使用呢?
原型模式(Prototype Design Pattern)
假若对象的创建成本较大,而此时我们需要创建一个与原对象差别不大(大部分属性值一致)的对象,我们可以通过已有对象进行复制的方式,创建新对象,可节省创建时间。这种创建对象的方式称为原型模式,原对象称为原型,复制操作称为拷贝。
原型模式的概念可能理解起来比较理论,我们通过一个场景例子来理解一下吧。试想一下,我们现在有一个数据库,保存了搜索词的相关信息(搜索次数、上次更新搜索次数的时刻等等)。系统中要求定时刷新搜索词的相关信息。示例代码如下:
public class SearchCache {
private Map<String, SearchInfo> cache = new ConcurrentHashMap<>();
public List<SearchInfo> loadSearchInfoFromDB() {
// 从数据库中加载全部搜索信息
}
public void refresh() {
List<SearchInfo> infos = loadSearchInfoFromDB();
if(CollectionUtils.isEmpty(infos)) {
return;
}
for(SearchInfo info : infos) {
cache.put(info.getKeyWord(), info);
}
}
}
有人可能会有疑问,每次都从数据库中加载所有的数据,这是没有必要,我们只需要加载需要更新的部分即可。我们可以对上面的代码稍作调整,用一个更新时间戳来区分更新阶段,因为每条记录都有一个更新时刻,只要更新时刻大于Cache的上次更新时间,这条数据就应该加载进内存刷新数据。调整后的示例代码如下:
public class SearchCache {
private long lastRefreshTime = 0L;
private Map<String, SearchInfo> cache = new ConcurrentHashMap<>();
public List<SearchInfo> loadSearchInfoFromDB(long lastRefreshTime) {
// 从数据库中加载增量更新的搜索信息
}
public void refresh() {
List<SearchInfo> infos = loadSearchInfoFromDB(lastRefreshTime);
if(CollectionUtils.isEmpty(infos)) {
return;
}
long nextRefreshTime = lastRefreshTime;
for(SearchInfo info : infos) {
// 更新缓存
cache.put(info.getKeyWord(), info);
// 计算下次刷新的更新时间戳
nextRefreshTime = Math.max(nextRefreshTime, info.getRefreshTime());
}
// 保存下次刷新的更新时间戳
lastRefreshTime = nextRefreshTime;
}
}
上述的代码,你可以发现,我们更新缓存 cache 时,是直接 for 循环更新。这里我们有一个场景,在循环更新的过程中,其他线程获取搜索信息,此时拿到的搜索信息的数据是部分更新的(此次更新只进行了部分),我们的需求是,当我们获取搜索信息时,拿到的数据是上个版本的,或者是最新版本的,而不是中间版本。我们怎么解决呢?示例代码如下:
public class SearchCache {
private long lastRefreshTime = 0L;
private Map<String, SearchInfo> cache = new ConcurrentHashMap<>();
public List<SearchInfo> loadSearchInfoFromDB(long lastRefreshTime) {
// 从数据库中加载增量更新的搜索信息
}
public void refresh() {
List<SearchInfo> infos = loadSearchInfoFromDB(lastRefreshTime);
if(CollectionUtils.isEmpty(infos)) {
return;
}
long nextRefreshTime = lastRefreshTime;
Map<String, SearchInfo> protoCache = cache.clone();// 浅拷贝
for(SearchInfo info : infos) {
protoCache.put(info.getKeyWord(), info);
// 计算下次刷新的更新时间戳
nextRefreshTime = Math.max(nextRefreshTime, info.getRefreshTime());
}
// 保存下次刷新的更新时间戳
lastRefreshTime = nextRefreshTime;
// 更新缓存
cache = protoCache;
}
}
上面的代码,使用了浅拷贝(关于浅拷贝、深拷贝的描述这里不做展开)解决了版本的问题,每次当我们获取搜索信息时,拿到的数据是上个版本的,或者是最新版本的,而不是中间版本。
除了 Object#clone 的方式外,我们还可以,现将对象序列化流,再将流反序列化成对象,示例代码如下:
public Object deepCopy(Object object) {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(object);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return oi.readObject();
}
原型模式是创建型模式的一种,通过克隆预生成的原型,而不是 new 实例,避免反复运行构造逻辑、初始化逻辑,也可以方便生成复杂对象,多用于创建复杂的或者耗时的实例。