# Prototype（原型模式）

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

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

## 举例子

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

### 做钥匙

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

### 两种状态表

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

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

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

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

### 模版组件

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

## 意图解释

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

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

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

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

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

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

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

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

## 结构图

<img width=800 src="https://img.alicdn.com/tfs/TB1roQlZWL7gK0jSZFBXXXZZpXa-1328-596.png">

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

## 代码例子

下面例子使用 typescript 编写。

```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 模式还是有类似之处的，在隐藏创建对象细节这一点上。

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

```typescript
const newComponent = oldComponent.clone()
```

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

## 弊端

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

原型模式的弊端：

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

## 总结

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

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

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

<img width=600 src="https://img.alicdn.com/tfs/TB1hBIdm6MZ7e4jSZFOXXX7epXa-982-486.png">

> 讨论地址是：[精读《设计模式 - Prototype 原型模式》· Issue #277 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/277)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
