# Factory Method（工厂方法）

Factory Method（工厂方法）属于创建型模式，利用工厂方法创建对象实例而不是直接用 New 关键字实例化。

理解如何写出工厂方法很简单，但理解为什么要用工厂方法就需要动动脑子了。工厂方法看似简单的将 New 替换为一个函数，其实是体现了面向接口编程的思路，它创建的对象其实是一个符合通用接口的通用对象，这个对象的具体实现可以随意替换，以达到通用性目的。

**意图：定义一个用于创建对象的接口，让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。**

## 举例子

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

### 换灯泡

我自己在家换过灯泡，以前我家里灯坏掉的时候，我看着这个奇形怪状的灯管，心里想，这种灯泡和这个灯座应该是一体的，市场上估计很难买到适配我这个灯座的灯泡了。结果等我把灯泡拧下来，跑到门口的五金店去换的时候，店员随便给了我一个灯泡，我回去随便拧了一下居然就能用了。

我买这个灯泡的过程就用到了工厂模式，而正是得益于这种模式，让我可以方便在家门口就买到可以用的灯泡。

### 卡牌对战游戏

卡牌对战中，卡牌有一些基本属性，比如攻防、生命值，也符合一些通用约定，比如一回合出击一起等等，那么对于战斗系统来说，应该怎样实例化卡牌呢？如何批量操作卡牌，而不是通用功能也要拿到每个卡牌的实例才能调用？另外每个卡牌有特殊能力，这些特殊能力又应该如何拓展呢？

### 实现任意图形拖拽系统

一个可以被交互操作的图形，它可以用鼠标进行拉伸、旋转或者移动，不同图形实现这些操作可能并不相同，要存储的数据也不一样，这些数据应该独立于图形存储，我们的系统如果要对接任意多的图形，具备强大拓展能力，对象关系应该如何设计呢？

## 意图解释

在使用工厂方法之前，我们就要创建一个 **用于创建对象的接口**，这个接口具备通用性，**所以我们可以忽略不同的实现来做一些通用的事情**。

换灯泡的例子来说，我去门口五金店买灯泡，而不是拿到灯泡材料自己 New 一个出来，就是因为五金店这个 “工厂” 提供给我的灯泡符合国家接口标准，而我家里的灯座也符合这个标准，所以灯座不需要知道对接的灯泡是具体哪个实例，什么颜色，什么形状，这些都无所谓，只要灯泡符合国家标准接口，就可以对接上。

对卡牌对战的系统来说，**所有卡牌都应该实现同一种接口**，所以卡牌对战系统拿到的卡牌应该就是简单的 Card 类型，这种类型具备基本的卡片操作交互能力，系统就调用这些能力完成基本流程就好了，如果系统直接实例化具体的卡片，那不同的卡片类型会导致系统难以维护，卡片间操作也无法抽象化。

正式这种模式，使得我们可以在卡牌的具体实现上做一些特殊功能，比如修改卡片攻击时效果，修改卡牌销毁时效果。

对图形拖拽系统来说，用到了 “连接平行的类层次” 这个特性，所谓连接平行的类层次，就是指一个图形，与其对应的操作类是一个平行抽象类，而一个具体的图形与具体的操作类则是另一个平行关系，系统只要关注最抽象的 “通用图形类” 与 “通用操作类” 即可，操作时，底层可能是某个具体的 “圆类” 与 “圆操作类” 结合使用，具体的类有不同的实现，但都符合同一种接口，因此操作系统才可以把它们一视同仁，统一操作。

**意图：定义一个用于创建对象的接口，让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。**

所以接口是非常重要的，工厂方法第一句话就是 “定义一个用于创建对象的接口”，这个接口就是 `Creator`，让子类，也就是具体的创建类（`ConcreteCreator`）决定要实例化哪个类（`ConcreteProduct`）。

所谓使一个类的实例化延迟到其子类，是因为抽象类不知道要实例化哪个具体类，所以实例化动作只能由具体的子类去做，这样绕一圈的好处是，我们可以将任意多对象看作是同一类事物，做统一的处理，比如 **无论何种灯泡实例都满足通用的灯座接口**，**所有工厂实例化的卡牌都具备玩一局卡牌游戏的基本功能**，**任何图形与交互类都满足特定功能关系**，这种思想让生活和设计得到了大幅简化。

## 结构图

<img width=800 src="https://img.alicdn.com/tfs/TB1VjyZmsVl614jSZKPXXaGjpXa-1434-476.png">

`Creator` 就是工厂方法，`ConcreteCreator` 是实现了 `Creator` 的具体工厂方法，每一个具体工厂方法生产一个具体的产品 `ConcreteProduct`，每个具体的产品都实现通用产品的特性 `Product`。

## 代码例子

下面例子使用 typescript 编写。

```typescript
// 产品接口
interface Product {
  save: () => void;
}

// 工厂接口
interface Creator {
  createProduct: () => Product;
}

// 具体产品
class ConcreteProduct implements Product {
  save = () => {};
}

// 具体工厂
class ConcreteCreator implements Creator {
  createProduct = () => {
    return new ConcreteProduct();
  };
}
```

创建一个 `Product` 的子类 `ConcreteCreator`，并返回一个实现了 `Product` 的具体实例 `ConcreteProduct`，这样我们就可以方便使用这个工厂了。

工厂方法并不是直接调用 `new ConcreteCreator().createProduct` 那么简单，这样体现不出任何抽象性，真正的场景是，在一个创建产品的流程中，我们只知道拿到的工厂是 `Creator`：

```typescript
function main(anyCreator: Creator) {
  const product = anyCreator.createProduct()
}
```

在外面调用 `main` 函数时，实际传进去的是一个具体工厂，比如 `myCreator`，但关键是 `main` 函数不用关心到底是哪一个具体工厂，只要知道是个工厂就行了，具体对象创建过程交给了其子类。

**你也许也发现了，这就是抽象工厂中其中的一步，所以抽象工厂使用了工厂方法。**

## 弊端

工厂方法中，每创建一种具体的子类，就要写一个对应的 `ConcreteCreate`，这相对比较笨重，但有意思的是，如果将创建多个对象放到一个 `ConcreteCreate` 中，就变成了 **简单工厂模式**，新增产品要修改已有类不符合开闭模式，反而推荐写成本文说的这种模式。

彼之毒药吾之蜜糖，要知道没有一种设计模式解决所有问题，没有一种设计模式没有弊端，**而这个弊端不代表这个设计模式不好，一个弊端的出现可能是为了解决另一个痛点。** 要接受不完美的存在，这么多种设计模式就是对应了不同的业务场景，**为合适的场景选择一种能将优势发扬光大，以至于能掩盖弊端，就算进行了合理的架构设计**。

## 总结

工厂方法并不是简单把 New 的过程换成了函数，而是抽象出一套面向接口的设计模式：

<img width=800 src="https://img.alicdn.com/tfs/TB1WKH.Zoz1gK0jSZLeXXb9kVXa-1480-786.png">

你看，我要做灯泡，可以直接做具体的灯泡，也可以定一个灯泡接口，通过灯泡工厂拿到具体灯泡，灯泡工厂对待所有灯泡的只做流程都是一样的，不管是中世纪风灯泡，还是复古灯泡，还是普通白织灯，都是一模一样的制作流程，具体怎么做由具体的子类去实现，这样我们可以统一管理 “灯泡” 这一个通用概念，而忽略不同灯泡之间不太重要的差别，程序的可维护性得到了大幅提升。

> 讨论地址是：[精读《设计模式 - Factory Method 工厂方法》· Issue #274 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/274)

**如果你想参与讨论，请 [点击这里](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)）
