首页 > 其他分享 >原型模式

原型模式

时间:2024-04-12 17:01:00浏览次数:10  
标签:newKeywords lastUpdateTime 模式 原型 searchWord currentKeywords 拷贝 SearchWord

1、原型模式的原理

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

  什么情况下创建对象的成本比较大呢?

  一般创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,但是如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

问题1

下面来看一个例子:

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

  关于这个问题,可以直接使用 HashMap 容器来实现。其中,HashMap 的 key 为搜索关键词,value 为关键词详细信息(比如搜索次数)。只需要将数据从数据库中读取出来,放入 HashMap 就可以了。不过,我们还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,在下面的示例图中,对 v2 版本的数据进行更新,得到 v3 版本的数据。假设只有更新和新添关键词,没有删除关键词的行为。

                               

  为了保证系统 A 中数据的实时性(不一定非常实时,但数据也不能太旧),系统 A 需要定期根据数据库中的数据,更新内存中的索引和数据。

  该如何实现这个需求呢?

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

public class Demo {
  private ConcurrentHashMap<String, SearchWord> currentKeywords = new ConcurrentHashMap<>();
  private long lastUpdateTime = -1;
​
  public void refresh() {
    // 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
        maxNewUpdatedTime = searchWord.getLastUpdateTime();
      }
      if (currentKeywords.containsKey(searchWord.getKeyword())) {
        currentKeywords.replace(searchWord.getKeyword(), searchWord);
      } else {
        currentKeywords.put(searchWord.getKeyword(), searchWord);
      }
    }
​
    lastUpdateTime = maxNewUpdatedTime;
  }
​
  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}

问题2

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

  该如何实现现在这个需求呢

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

public class Demo {
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
​
  public void refresh() {
    HashMap<String, SearchWord> newKeywords = new LinkedHashMap<>();
​
    // 从数据库中取出所有的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords();
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
      newKeywords.put(searchWord.getKeyword(), searchWord);
    }
​
    currentKeywords = newKeywords;
  }
​
  private List<SearchWord> getSearchWords() {
    // TODO: 从数据库中取出所有的数据
    return null;
  }
}

问题3

  在问题2的代码实现中,newKeywords 构建的成本比较高。需要将这 10 万条数据从数据库中读出,然后计算哈希值,构建 newKeywords。这个过程显然是比较耗时。为了提高效率,原型模式就派上用场了。

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

public class Demo {
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;
​
  public void refresh() {
    // 原型模式就这么简单,拷贝已有对象的数据,更新少量差值
    HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
​
    // 从数据库中取出更新时间>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.getKeyword())) {
        SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword());
        oldSearchWord.setCount(searchWord.getCount());
        oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
      } else {
        newKeywords.put(searchWord.getKeyword(), searchWord);
      }
    }
​
    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }
​
  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}

  上面的代码中利用了 Java 的 clone() 语法来复制一个对象。其实把数据从 currentKeywords 中一个个取出来,然后再重新计算哈希值,放入到 newKeywords 中也是可以接受的。毕竟,最耗时的还是从数据库中取数据的操作。相对于数据库的 IO 操作来说,内存操作和 CPU 计算的耗时都是可以忽略的。

  其实上面的代码也存在问题,这就涉及到原型模式的实现方式了。

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

  在内存中,用散列表组织的搜索关键词信息是如何存储的。大致结构如下所示。从图中可以发现,散列表索引中,每个结点存储的 key 是搜索关键词,value 是 SearchWord 对象的内存地址。SearchWord 对象本身存储在散列表之外的内存空间中。

                                            

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

                         

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

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

  又该如何来解决这个问题呢?

  可以将浅拷贝替换为深拷贝。newKeywords 不仅仅复制 currentKeywords 的索引,还把 SearchWord 对象也复制一份出来,这样 newKeywords 和 currentKeywords 就指向不同的 SearchWord 对象,也就不存在更新 newKeywords 的数据会导致 currentKeywords 的数据也被更新的问题了。

  实现深拷贝有两种方法:

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

public class Demo {
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;
​
  public void refresh() {
    // Deep copy
    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的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
        maxNewUpdatedTime = 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);
      }
    }
​
    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }
​
  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
​
}

第二种是序列化:先将对象序列化,然后再反序列化成新的对象。具体的示例代码如下所示:

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();
}

  上面两种实现方法,深拷贝都要比浅拷贝耗时、耗内存空间。针对这个应用场景,有没有更快、更省内存的实现方式呢?

  可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,我们再使用深度拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象。毕竟需要更新的数据是很少的。这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的数据都是老版本的数据。具体的代码实现如下所示:

public class Demo {
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;
​
  public void refresh() {
    // Shallow copy
    HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
​
    // 从数据库中取出更新时间>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.getKeyword())) {
        newKeywords.remove(searchWord.getKeyword());
      }
      newKeywords.put(searchWord.getKeyword(), searchWord);
    }
​
    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }
​
  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}

 

 

标签:newKeywords,lastUpdateTime,模式,原型,searchWord,currentKeywords,拷贝,SearchWord
From: https://www.cnblogs.com/jing-yi/p/18131674

相关文章

  • 二手交易平台原型设计
    一:墨刀、Axure、Mockplus等原型设计工具的各自的适用领域及优缺点。1.墨刀:适用领域:适合用于APP和微信小程序的原型设计。优点:能够控制组件,并且模板自动匹配调整大小,使得设计体验流畅。同时操作简单,界面友好。具有丰富的组件库和模板库,可快速创建各种类型的原型。缺点:交互的深......
  • 实验1 原型设计
    一、实验题目:原型设计二、实验目的:掌握产品原型设计方法和相应工具使用。三、实验要求(1)对比分析墨刀、Axure、Mockplus等原型设计工具的各自的适用领域及优缺点。墨刀(Modao)适用领域:墨刀是一款在线的原型设计工具,主要用于移动应用和网页设计的原型设计。它非常适合快速原型......
  • 时光音乐原型设计
    目录一、原型设计工具对比1.Axure优点:缺点:2.墨刀优点:缺点:3.Mockplus优点:缺点:4.对以上三款工具优缺点总结二、对音乐原型设计因素说明原型图页面展示1.登录界面说明2.首页界面说明3.播放界面说明4.我的界面说明时光音乐原型设计一、原型设计工具对比1.Axure优点:1.功能强大,支持......
  • Java策略模式实践
    1什么是策略模式策略模式(StrategyPattern):一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改......
  • 李彦宏:闭源才有真正商业模式才能聚集算力和人才;史上首位数学和计算机最高奖双得主丨RT
       开发者朋友们大家好: 这里是「RTE开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享RTE(RealTimeEngagement)领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「有看点的会议」,但内容仅代表编......
  • C++观察者模式的实现
    C++观察者模式的实现观察者模式介绍观察者模式是软件设计模式里面一种很常用又很重要的一种设计模式,观察者模式又叫做发布-订阅(Publish/Subscribe)模式。也就是主题对象(subject)发布通知,订阅该主题的多个观察者(observer)可以收到通知从而更新自己。主题对象Subject发出通知时并不......
  • 消息中间件RabbitMQ_RabbitMQ的工作模式4
    一、Workqueues工作队列模式1、模式说明WorkQueues:与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。2、代码编写WorkQueues与入门程序的简......
  • 软件工程实验1-产品原型设计
    一、实验题目:原型设计二、实验目的:掌握产品原型设计方法和相应工具使用三、实验要求(1)分析各工具◇======墨刀(MockingBot):○---适用领域:界面设计和原型制作,适合快速搭建移动应用和网页的界面。团队协作,支持多人实时编辑和评论。设计系统构建,可以创建组件库和样式指南。交......
  • 汇编语言简易教程(8):寻址模式
    汇编语言简易教程(8):寻址模式寻址模式是使用正在访问(读取或写入)的数据项的地址来访问内存中的值的受支持方法。这可能包括变量的名称或数组中的位置。基本的寻址模式包含:寄存器立即数内存寻址注意事项使用[]需要注意:访问内存的唯一方法是使用方括号([]'s)。省略括号......
  • 汇编语言简易教程(8):寻址模式
    汇编语言简易教程(8):寻址模式寻址模式是使用正在访问(读取或写入)的数据项的地址来访问内存中的值的受支持方法。这可能包括变量的名称或数组中的位置。基本的寻址模式包含:寄存器立即数内存寻址注意事项使用[]需要注意:访问内存的唯一方法是使用方括号([]'s)。省略括号......