专栏首页WindCoder漫谈原型模式

漫谈原型模式

1. 什么是

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。

1.1 对象的创建成本比较大的场景

创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。

但如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

1.2 最快速地clone一个HashMap散列表

以如何最快速地clone一个HashMap散列表为例。

当需要将数据库存储的搜索关键字信息存入内存以备后续需求调用。可以直接使用Java语言中提供的 HashMap 容器来实现。其中,HashMap 的 key 为搜索关键词,value 为关键词详细信息(比如搜索次数)。我们只需要将数据从数据库中读取出来,放入 HashMap 就可以了。

当另一个系统B同时需要操作该关键字信息数据时,为了保证系统 A 中数据的实时性,只需要在系统 A 中,记录当前数据的版本 Va 对应的更新时间 Ta,从数据库中捞出更新时间大于 Ta 的所有搜索关键词,也就是找出 Va 版本与最新版本数据的“差集”,然后针对差集中的每个关键词进行处理。存在则更新,不存在就插入。

当存在更新的需求,如1、 任何时刻,系统 A 中的所有数据都必须是同一个版本的;2、同时在更新内存数据的时候,系统 A 不能处于不可用状态,也就是不能停机更新数据时,解决方案如下:把正在使用的数据的版本定义为“服务版本”,当要更新内存中的数据的时候,并不直接在服务版本(假设是版本 a 数据)上更新,而是重新创建另一个版本数据(假设是版本 b 数据),等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。

但这样做新版本(newKeywords )的构建的成本比较高:从数据库中读出,然后计算哈希值,构建 newKeywords,过程会比较耗时。

此时原型模式便可解决该问题。

2. 实现

原型模式基于拷贝已有对象的数据(深拷贝和浅拷贝)实现。

浅拷贝和深拷贝的区别在于:

  • 浅拷贝只会复制索引(散列表),不会复制数据(SearchWord 对象)本身
  • 深拷贝不仅仅会复制索引,还会复制数据本身。故深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。

2.1 浅拷贝实现

在 Java 语言中,Object 类的 clone() 方法执行的就是我们刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。

  • 如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的
  • 对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了
/**
 * 原型模式--浅拷贝
 *
 *  利用Java 中的 clone() 语法来复制一个对象。
 *
 * 最耗时的还是从数据库中取数据的操作。相对于数据库的 IO 操作来说,内存操作和 CPU 计算的耗时都是可以忽略的。
 *
 * 此时处于浅拷贝,当我们通过 newKeywords 更新 SearchWord 对象的时候,newKeywords 和 currentKeywords 因为指向相同的一组 SearchWord 对象,
 * 就会导致 currentKeywords 中指向的 SearchWord,有的是老版本的,有的是新版本的
 * 从而无法满足:currentKeywords 中的数据在任何时刻都是同一个版本的,不存在介于老版本与新版本之间的中间状态
 *
 * 解决方案是改用深拷贝实现。
 */
public class PrototypeDemo3 {
    private HashMap<String, SearchWord> currentKeywords = new HashMap<String, SearchWord>();
    private long lastUpateTime = -1;

    public void refresh() {
        // 原型模式 拷贝已有对象的数据,更新少量差值
        HashMap<String, SearchWord> newKeyWords = (HashMap<String, SearchWord>) currentKeywords.clone();

        // 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpateTime);
        long maxNewUpdateTime = lastUpateTime;

        for (SearchWord searchWord: toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdateTime) {
                maxNewUpdateTime = searchWord.getLastUpdateTime();
            }
            if (newKeyWords.containsKey(searchWord.getKeyword())) {
                // 存在直接更新
                SearchWord oldSearchWOrd = newKeyWords.get(searchWord.getKeyword());
                oldSearchWOrd.setCount(searchWord.getCount());
                oldSearchWOrd.setLastUpdateTime(searchWord.getLastUpdateTime());
            } else {
                // 不存在就加入
                newKeyWords.put(searchWord.getKeyword(), searchWord);
            }

        }
        lastUpateTime = maxNewUpdateTime;
        currentKeywords = newKeyWords;

    }

    private List<SearchWord> getSearchWords(long lastUpateTime) {
        // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
         return null;
    }
}

2.2 深拷贝实现

深拷贝实现有两种方案:递归拷贝和对象序列化

2.2.1 递归拷贝

第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。根据这个思路对之前的代码进行重构。

/**
 * 原型模式--深拷贝-递归拷贝对象
 *
 * 递归拷贝对象、对象的引用对象以及引用对象的引用对象……
 * 直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。
 * 根据这个思路对之前的代码进行重构
 */
public class PrototypeDemo4 {
    private HashMap<String, SearchWord> currentKeywords = new HashMap<String, SearchWord>();
    private long lastUpateTime = -1;

    public void refresh() {
        HashMap<String, SearchWord> newKeyWords = new HashMap<>();
        for (HashMap.Entry<String, SearchWord> e: currentKeywords.entrySet()) {
            SearchWord searchWord = e.getValue();
            SearchWord newSearchWord = new SearchWord(searchWord.getKeyword(),
                    searchWord.getCount(), searchWord.getLastUpdateTime());
            newKeyWords.put(e.getKey(), newSearchWord);
        }

        // 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpateTime);
        long maxNewUpdateTime = lastUpateTime;

        for (SearchWord searchWord: toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdateTime) {
                maxNewUpdateTime = searchWord.getLastUpdateTime();
            }
            if (newKeyWords.containsKey(searchWord.getKeyword())) {
                SearchWord oldSearchWOrd = newKeyWords.get(searchWord.getKeyword());
                oldSearchWOrd.setCount(searchWord.getCount());
                oldSearchWOrd.setLastUpdateTime(searchWord.getLastUpdateTime());

            } else {
                newKeyWords.put(searchWord.getKeyword(), searchWord);
            }

        }
        lastUpateTime = maxNewUpdateTime;
        currentKeywords = newKeyWords;

    }

    private List<SearchWord> getSearchWords(long lastUpateTime) {
        // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
         return null;
    }
}

2.2.2 对象序列化与反序列化

第二种方法:先将对象序列化,然后再反序列化成新的对象

/**
 * 原型模式--深拷贝-对象序列化
 *
 * 仅用于提供思路参考,不保证代码本身正确性以及正常运行。
 */
public class PrototypeDemo5 {
    private HashMap<String, SearchWord> currentKeywords = new HashMap<String, SearchWord>();
    private long lastUpateTime = -1;

    public void refresh() {
        HashMap<String, SearchWord> newKeyWords = new HashMap<>();
        for (HashMap.Entry<String, SearchWord> e: currentKeywords.entrySet()) {
            SearchWord searchWord = e.getValue();
            SearchWord newSearchWord = new SearchWord(searchWord.getKeyword(),
                    searchWord.getCount(), searchWord.getLastUpdateTime());
            newKeyWords.put(e.getKey(), newSearchWord);
        }

        // 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpateTime);
        long maxNewUpdateTime = lastUpateTime;

        for (SearchWord searchWord: toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdateTime) {
                maxNewUpdateTime = searchWord.getLastUpdateTime();
            }
            if (newKeyWords.containsKey(searchWord.getKeyword())) {
                SearchWord oldSearchWOrd = null;
                try {
                    oldSearchWOrd = (SearchWord) deepCopy(newKeyWords.get(searchWord.getKeyword()));
                    oldSearchWOrd.setCount(searchWord.getCount());
                    oldSearchWOrd.setLastUpdateTime(searchWord.getLastUpdateTime());
                    newKeyWords.replace(oldSearchWOrd.getKeyword(), oldSearchWOrd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            } else {
                newKeyWords.put(searchWord.getKeyword(), searchWord);
            }

        }
        lastUpateTime = maxNewUpdateTime;
        currentKeywords = newKeyWords;

    }

    /**
     * 先将对象序列化,然后再反序列化成新的对象
     * @param object
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public Object deepCopy(Object object) throws IOException, ClassNotFoundException {
        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();
    }

    private List<SearchWord> getSearchWords(long lastUpateTime) {
        // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
         return null;
    }
}

2.3 深浅拷贝结合

无论单纯使用深拷贝的哪种实现方式,深拷贝都要比浅拷贝耗时、耗内存空间。

为解决该问题,可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,我们再使用深度拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象

需要更新的数据是很少的。这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的中数据都是老版本的数据。

/**
 *  原型模式--深浅拷贝结合
 */
public class PrototypeDemo6 {
    private HashMap<String, SearchWord> currentKeywords = new HashMap<String, SearchWord>();
    private long lastUpateTime = -1;

    public void refresh() {
        // 浅拷贝获取所有对象
        HashMap<String, SearchWord> newKeyWords = (HashMap<String, SearchWord>) currentKeywords.clone();


        // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeyWords中
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpateTime);
        long maxNewUpdateTime = lastUpateTime;

        for (SearchWord searchWord: toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdateTime) {
                maxNewUpdateTime = searchWord.getLastUpdateTime();
            }
            if (newKeyWords.containsKey(searchWord.getKeyword())) {
                newKeyWords.remove(searchWord.getKeyword());
            }
            newKeyWords.put(searchWord.getKeyword(), searchWord);

        }
        lastUpateTime = maxNewUpdateTime;
        currentKeywords = newKeyWords;

    }

    private List<SearchWord> getSearchWords(long lastUpateTime) {
        // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
         return null;
    }
}

3. 优缺点

该部分来自《Head First设计模式》,有的地方可能过于抽象或官方语言,仅作相关参考。

3.1 优点

  • 向客户隐藏制造新实例的复杂性。
  • 提供让客户能够产生未知类型对象的选项。
  • 在某些环境下,复制对象比创建对象更有效。

3.2 用途和缺点

  • 在一个复杂的类层次中,当系统必须从其中的许多类型创建新对象时(即,当创建给定类的实例的过程很昂贵或者很复杂时),可以考虑原型模式。
  • 使用原型模式的缺点:对象的复制有时相当复杂。

参考资料

相关下载

点击下载

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 单例模式(下)

    在上篇 《单例模式(上)》一文中介绍了单例定义、使用场景、实现方式以及不足,本篇继续整理针对不足的解决方案以及唯一性的相关讨论与实现等。

    汐楓
  • JVM-HotSpot虚拟机对象探秘

    虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。若没有,则...

    汐楓
  • CDN静态资源加速

    静态资源访问的关键点是就近访问。可以考虑在业务服务器的上层加一层特殊缓存,即CDN。

    汐楓
  • Java 集合框架(7)---- Set 相关类解析

    在上篇文章中,我们将剩下的常见的 Map 接口下的相关具体类做了一个解析,还有一些相关的类将会在下一篇文章中做一个总结,这篇我们来看看 Set 接口的相关类。老...

    指点
  • Android系统的智能指针(轻量级指针、强指针和弱指针)的实现原理分析【转】

    Android系统的运行时库层代码是用C++来编写的,用C++ 来写代码最容易出错的地方就是指针了,一旦使用不当,轻则造成内存泄漏,重则造成系统崩溃。不过系统为...

    233333
  • 海思HI3559A硬件说明出炉

    天睿视迅
  • React新特性——Protals与Error Boundaries

    在React 16.x 新增了一个名为“Protals”的特性,直接按照字面意思翻译实在不靠谱。在描述这个特性时,我们还是用官方的英文单词来指定它。Portal...

    随风溜达的向日葵
  • FLEX 3 有关视频加载处理1

    1. 创建一个NetConnection 对象,它的作用是连接到远程服务器,调用命令,播放视频

    py3study
  • HFish蜜罐使用心得

    最近搭建各种蜜罐测试,这篇文章主要分享 HFish V0.4 使用过程中的一些心得。

    FB客服
  • ES6中的几个常用特性

    5.Enhanced Object Literals (增强的对象字面量)in ES6

    javascript.shop

扫码关注云+社区

领取腾讯云代金券