2022-09-21设计模式00
请注意,本文编写于 554 天前,最后修改于 554 天前,其中某些信息可能已经过时。

Prototype(原型模式)

Prototype(原型模式)属于创建型模式,既不是工厂也不是直接 New,而是以拷贝的方式创建对象。

意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

举例子

如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。

做钥匙

很显然,为了房屋安全,要尽量做到一把钥匙只能开一扇门,每把钥匙结构都多多少少不一样,却又很相似,做钥匙的人按照你给的钥匙一模一样做一个新的,这属于什么模式呢?

两种状态表

当网站做不停机维护时,假设维护内容是给每个高级会员账户多打 100 元现金,现在需要改数据库表。已知:

  1. 数据库表有几千万条数据,其中高级会员有几千位,为了方便调用已经缓存在中间层了,且数据库对应 ID 更新后对应缓存也会更新。
  2. 几千条数据修改语句执行完需要几分钟,这几分钟内无法接受用户数据不同步的问题。

一种常见的做法是,我们生成一份高级会员列表的拷贝,代替数据库缓存的结果,数据库只要读到对应会员 ID 就从拷贝列表中获取,数据表新增一列状态标志,操作完后这个拷贝移除,更新高级会员缓存。

但是如何生成高级会员列表拷贝呢?如果直接从几千万条用户数据中重新查询,会有较高的数据库查询成本。

模版组件

通用搭建系统中,我们可以将某个拖拽到页面的区块设置为 “模版”,这个模版可以作为一个新组件被重新拖拽到任意位置,实例化任意次。实际上,这是一种分段式复制粘贴,你会如何实现这个功能呢?

意图解释

解决上面问题的办法都很简单,就是基于已有对象进行复制即可,效率比 New 一个,或者工厂模式都要高。

意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

所谓原型实例,就是被选为拷贝模版的那个对象,比如做钥匙例子中,你给老板的样板钥匙;两种状态表中的已有缓存高级会员列表;模版组件中选中的那个组件。然后,通过拷贝这些原型创建你想要的对象即可。

我们抽象思考一下,如果每把钥匙都遵循 Prototype 接口,提供了 clone() 方法以复制自己,那就可以快速复制任意一把钥匙。钥匙工厂可无法解决每把钥匙不一样的问题,我们要的就是和某个钥匙一模一样的副本,复制一份钥匙最简单。

高级会员状态表例子中,查询数据库的成本是高昂的,但如果仅仅复制已经查询好的列表,时间可以忽略不计,因此最经济的方案是直接复制,而不是通过工厂模式重新连接数据库并执行查询。

模版组件更是如此,我们根本没有定义那么多组件实例的基类,只要每个组件提供一个 clone() 函数,就可以立即复制任意组件实例,这无疑是最经济实惠的方案。

看到这里,你应该知道了,原型模式的精髓是对象要提供 clone() 方法,而这个 clone() 方法实现难度有高有低。

一般来说,原型模式的拷贝建议用深拷贝,毕竟新对象最好不要影响到旧对象,但是在深拷贝性能问题较大的情况下,可以考虑深浅拷贝结合,也就是将在新对象中,不会修改的数据使用浅拷贝,可能被修改的数据使用深拷贝。

结构图

Client 是发出指令的客户端,Prototype 是一个接口,描述了一个对象如何克隆自身,比如必须拥有 clone() 方法,而 ConcretePrototype 就是克隆具体的实现,不同对象有不同的实现来拷贝自身。

代码例子

下面例子使用 typescript 编写。

class Component implements Prototype {
  /**
   * 组件名
   */
  private name: string
  /**
   * 组件版本
   */
  private version: string

  /**
   * 拷贝自身
   */
  public clone = () => {
    // 构造函数省略了,大概就是传递 name 和 version
    return new Component(this.name, this.version)
  }
}

我们可以看到,实现了 Prototype 接口的 Component 必须实现 clone 方法,这样任意组件在执行复制时,就可以直接调用 clone 函数,而不用关心每个组件不同的实现方式了。

从这就能看出,原型模式与 Factory 与 Builder 模式还是有类似之处的,在隐藏创建对象细节这一点上。

使用的时候,我们就可以这样创建一个新对象:

const newComponent = oldComponent.clone()

这里有两个注意点:一般来说,如果要二次修改生成的对象,不建议给 clone 函数加参数,因为这样会导致接口的不一致。 我们可以为对象实例提供一些 set 函数进行二次修改。另外,clone 函数要考虑性能,就像前面说过的,可以考虑深浅拷贝结合的方式,同时要注意当对象存在引用关系甚至循环引用时,甚至不一定能实现拷贝函数。

弊端

每个设计模式必有弊端,但就像每一期都说的,有弊端不代表设计模式不好用,而是指在某种场景喜爱存在问题,我们只要规避这些场景,在合理的场景使用对应设计模式即可。

原型模式的弊端:

  1. 每个类都要实现 clone 方法,对类的实现是有一定入侵的,要修改已有类时,违背了开闭原则。
  2. 当类又调用了其他对象时,如果要实现深拷贝,需要对应对象也实现 clone 方法,整体链路可能会特别长,实现起来比较麻烦。

总结

原型模式一般与工厂模式搭配使用,一般工厂方法接收一个符合原型模式的实例,就可以调用它的 clone 函数创建返回新对象啦。 代码大概是这样:

// buildComponentFactory 内部通过 targetComponent.clone() 创建对象,而不是 New 或者调用其他工厂函数。
const newComponent = buildComponentFactory(new Component())

最后来一张图快速理解原型模式:

关注 前端面试进阶微信公众号

本文作者:前端小毛

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!