Prototype(原型模式)属于创建型模式,既不是工厂也不是直接 New,而是以拷贝的方式创建对象。
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
很显然,为了房屋安全,要尽量做到一把钥匙只能开一扇门,每把钥匙结构都多多少少不一样,却又很相似,做钥匙的人按照你给的钥匙一模一样做一个新的,这属于什么模式呢?
当网站做不停机维护时,假设维护内容是给每个高级会员账户多打 100 元现金,现在需要改数据库表。已知:
一种常见的做法是,我们生成一份高级会员列表的拷贝,代替数据库缓存的结果,数据库只要读到对应会员 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
函数要考虑性能,就像前面说过的,可以考虑深浅拷贝结合的方式,同时要注意当对象存在引用关系甚至循环引用时,甚至不一定能实现拷贝函数。
每个设计模式必有弊端,但就像每一期都说的,有弊端不代表设计模式不好用,而是指在某种场景喜爱存在问题,我们只要规避这些场景,在合理的场景使用对应设计模式即可。
原型模式的弊端:
clone
方法,对类的实现是有一定入侵的,要修改已有类时,违背了开闭原则。clone
方法,整体链路可能会特别长,实现起来比较麻烦。原型模式一般与工厂模式搭配使用,一般工厂方法接收一个符合原型模式的实例,就可以调用它的 clone
函数创建返回新对象啦。 代码大概是这样:
// buildComponentFactory 内部通过 targetComponent.clone() 创建对象,而不是 New 或者调用其他工厂函数。
const newComponent = buildComponentFactory(new Component())
最后来一张图快速理解原型模式: