10

精读《设计模式 - Prototype 原型模式》

 3 years ago
source link: https://zhuanlan.zhihu.com/p/268707945
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

精读《设计模式 - Prototype 原型模式》

前端开发话题下的优秀回答者

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

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

讨论地址是:精读《设计模式 - Prototype 原型模式》· Issue #277 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK