侧边栏壁纸
博主头像
qingtian博主等级

喜欢是一件细水流长的事,是永不疲惫的双向奔赴~!

  • 累计撰写 104 篇文章
  • 累计创建 48 个标签
  • 累计收到 1 条评论

再谈原型模式

qingtian
2022-04-05 / 0 评论 / 0 点赞 / 678 阅读 / 7,171 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-04-05,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

原型模式

原型模式的原理和应用

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

什么情况下创建一个对象的消耗较大呢?
通常情况下,创建对象包含的申请内存,给成员变量赋值的这一过程,不会花费太多时间,如果对象中的数据需要通过复杂的计算才能得到(比如排序,计算哈希值),或者需要从RPC、网络、数据库、文件系统等非常慢速的IO系统中读取。这种情况下我们可以利用原型模式,从已有的其他对象中直接拷贝得到,而不用每次都创建新的对象。

假设数据库中存储了大约 10 万条“搜索关键词”信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统 A 在启动的时候会加载这份数据到内存中,用于处理某些其他的业务需求。为了方便快速地查找某个关键词对应的信息,我们给关键词建立一个散列表索引。

如果你熟悉的是 Java 语言,可以直接使用语言中提供的 HashMap 容器来实现。其中,HashMap 的 key 为搜索关键词,value 为关键词详细信息(比如搜索次数)。我们只需要将数据从数据库中读取出来,放入 HashMap 就可以了。

不过,我们还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。

为了保证系统A数据的实时性(可以容忍,但不能太旧),系统A需要定期从数据库中获取数据,更新进内存。

我们只需要在系统 A 中,记录当前数据的版本 Va 对应的更新时间 Ta,从数据库中捞出更新时间大于 Ta 的所有搜索关键词,也就是找出 Va 版本与最新版本数据的“差集”,然后针对差集中的每个关键词进行处理。如果它已经在散列表中存在了,我们就更新相应的搜索次数、更新时间等信息;如果它在散列表中不存在,我们就将它插入到散列表中。

public class Demo {
    private ConcurrentHashMap<String,SearchWord> concurrentKeyWords = new ConcurrentHashMap<>();
    private long lastUpdateTime = -1;

    public void refresh() {
        //从数据库取出更新时间 > lastUpdateTime的数据,放入到concurrentKeyWords中
        List<SearchWord> toBeUpdateSearchWords = getSearchWords(lastUpdateTime);
        long maxNewUpdatedTime = lastUpdateTime;
        for (SearchWord toBeUpdateSearchWord : toBeUpdateSearchWords) {
            if (toBeUpdateSearchWord.getLastUpdateTime() > lastUpdateTime) {
                maxNewUpdatedTime = toBeUpdateSearchWord.getLastUpdateTime();
            }
            if (concurrentKeyWords.contains(toBeUpdateSearchWord.getKeyWords())) {
                concurrentKeyWords.replace(toBeUpdateSearchWord.getKeyWords(),toBeUpdateSearchWord);
            }else {
                concurrentKeyWords.put(toBeUpdateSearchWord.getKeyWords(),toBeUpdateSearchWord);
            }
        }
        lastUpdateTime = maxNewUpdatedTime;
    }

    private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // 从数据库获取更新时间 > lastUpdateTime的数据 ,逻辑
        return null;
    }
}

如果要求:任何时刻,系统 A 中的所有数据都必须是同一个版本的,要么都是版本 a,要么都是版本 b,不能有的是版本 a,有的是版本 b。那刚刚的更新方式就不能满足这个要求了。除此之外,我们还要求:在更新内存数据的时候,系统 A不能处于不可用状态,也就是不能停机更新数据。

我们把正在使用的数据的版本定义为“服务版本”,当我们要更新内存中的数据的时候,我们并不是直接在服务版本(假设是版本 a 数据)上更新,而是重新创建另一个版本数据(假设是版本 b 数据),等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。这样既保证了数据一直可用,又避免了中间状态的存在。

实现代码如下:

public class Demo {
    private HashMap<String, SearchWord> concurrentKeyWords = new HashMap<>();

    public void refresh() {

        HashMap<String, SearchWord> newKeyWords = new LinkedHashMap<>();
        //从数据库取出更新时间 > lastUpdateTime的数据,放入到concurrentKeyWords中
        List<SearchWord> toBeUpdateSearchWords = getSearchWords();
        for (SearchWord searchWord : toBeUpdateSearchWords) {
            newKeyWords.putIfAbsent(searchWord.getKeyWords(),searchWord);
        }
        concurrentKeyWords = newKeyWords;
    }

    private List<SearchWord> getSearchWords() {
        // 从数据库获取更新时间 > lastUpdateTime的数据 ,逻辑
        return null;
    }
}

上面的代码实现中,newWords的构建的成本比较高。我们需要将这些数据从数据库中读出来,然后计算哈希值,放入newWords容器中。这种场景下,我们可以使用原型模式。

我们拷贝 currentKeywords 数据到 newKeywords 中,然后从数据库中只捞出新增或者有更新的关键词,更新到 newKeywords 中。而相对于 10 万条数据来说,每次新增或者更新的关键词个数是比较少的,所以,这种策略大大提高了数据更新的效率。

按照这个设计思路,代码实现如下:

public class Demo {
    private HashMap<String, SearchWord> concurrentKeyWords = new HashMap<>();
    private long lastUpdateTime = -1;

    public void refresh() {
        HashMap<String, SearchWord> newKeyWords = (HashMap<String, SearchWord>) concurrentKeyWords.clone();
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
        long maxNewUpdatedTime = lastUpdateTime;
        for (SearchWord searchWord : toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
                maxNewUpdatedTime = searchWord.getLastUpdateTime();
            }
            if (newKeyWords.containsKey(searchWord.getKeyWords())) {
                SearchWord oldSearchWord = newKeyWords.get(searchWord.getKeyWords());
                oldSearchWord.setCount(searchWord.getCount());
                oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
            }else {
                newKeyWords.put(searchWord.getKeyWords(),searchWord);
            }
        }
        lastUpdateTime = maxNewUpdatedTime;
        concurrentKeyWords = newKeyWords;
    }

    private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // 从数据库获取更新时间 > lastUpdateTime的数据 ,逻辑
        return null;
    }
}

原型模式的实现方式:深拷贝和浅拷贝

浅拷贝和深拷贝的区别在于,浅拷贝只会复制图中的索引(散列表),不会复制数据
(SearchWord 对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据
(SearchWord 对象),而深拷贝得到的是一份完完全全独立的对象。

在Java语言中,Object类的clone()方法执行的是浅拷贝,它只会拷贝对象中的基本数据类型,以及引用对象内存地址,而不会去复制一份对象。

在上面的代码中,我们通过调用 HashMap 上的 clone() 浅拷贝方法来实现原型模式。当我们通过 newKeywords 更新 SearchWord 对象的时候(比如,更新“设计模式”这个搜索关键词的访问次数),newKeywords 和 currentKeywords 因为指向相同的一组SearchWord 对象,就会导致 currentKeywords 中指向的 SearchWord,有的是老版本的,有的是新版本的,就没法满足我们之前的需求:currentKeywords 中的数据在任何时刻都是同一个版本的,不存在介于老版本与新版本之间的中间状态。

为了解决这种问题,我们可以使用深拷贝代替浅拷贝。
代码如下:

一、递归地去拷贝对象

public class Demo {
    private HashMap<String, SearchWord> concurrentKeyWords = new HashMap<>();
    private long lastUpdateTime = -1;

    public void refresh() {
        //Deep copy
        HashMap<String, SearchWord> newKeyWords = new HashMap<>();
        for (Map.Entry<String, SearchWord> e : concurrentKeyWords.entrySet()) {
            SearchWord searchWord = e.getValue();
            SearchWord newSearchWord = new SearchWord(searchWord.getLastUpdateTime(),searchWord.getKeyWords(),searchWord.getCount());
            newKeyWords.put(e.getKey(),newSearchWord);
        }

        //从数据库取出更新时间 > lastUpdateTime的数据,放入到newKeyWords中
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
        long maxNewUpdatedTime = lastUpdateTime;
        for (SearchWord searchWord : toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
                maxNewUpdatedTime = searchWord.getLastUpdateTime();
            }
            if (newKeyWords.containsKey(searchWord.getKeyWords())) {
                SearchWord oldSearchWord = newKeyWords.get(searchWord.getKeyWords());
                oldSearchWord.setCount(searchWord.getCount());
                oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
            }else {
                newKeyWords.put(searchWord.getKeyWords(),searchWord);
            }
        }
        lastUpdateTime = maxNewUpdatedTime;
        concurrentKeyWords = newKeyWords;
    }

    private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // 从数据库获取更新时间 > lastUpdateTime的数据 ,逻辑
        return null;
    }
}

二、先将对象序列化,然后反序列化成新的对象。

public Object deepCopy(Object object) {
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(object);

        ByteArrayInputStream bi = new ByteArrayInputStream();
        ObjectInputStream oi = new ObjectInputStream(bi);
        return oi.readObject();
    }

但是使用深拷贝都要比浅拷贝耗时、耗内存空间。

我们可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,我们再使用深度拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象。毕竟需要更新的数据是很少的。

代码如下:

public class Demo {
    private HashMap<String, SearchWord> concurrentKeyWords = new HashMap<>();
    private long lastUpdateTime = -1;

    public void refresh() {
        HashMap<String, SearchWord> newKeyWords = (HashMap<String, SearchWord>) concurrentKeyWords.clone();
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
        long maxNewUpdatedTime = lastUpdateTime;
        for (SearchWord searchWord : toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > lastUpdateTime) {
                maxNewUpdatedTime = searchWord.getLastUpdateTime();
            }
            if (newKeyWords.containsKey(searchWord.getKeyWords())) {
                newKeyWords.remove(searchWord.getKeyWords());
            }
            newKeyWords.put(searchWord.getKeyWords(),searchWord);
        }
        lastUpdateTime = maxNewUpdatedTime;
        concurrentKeyWords = newKeyWords;
    }

    private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // 从数据库获取更新时间 > lastUpdateTime的数据 ,逻辑
        return null;
    }

}

总结

原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。

如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。除非像我们今天实战中举的那个例子,需要从数据库中加载 10 万条数据并构建散列表索引,操作非常耗时,比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。

0

评论区