# Abstract Factory（抽象工厂）

Abstract Factory（抽象工厂）属于创建型模式，工厂类模式抽象程度从低到高分为：简单工厂模式 -> 工厂模式 -> 抽象工厂模式。

**意图：提供一个接口以创建一系列相关或相互依赖的对象，而无须指定它们具体的类。**

## 举例子

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

### 汽车工厂

我们都知道汽车有很多零部件，随着工业革命带来的分工，很多零件都可以被轻松替换。但实际生活中我们消费者不愿意这样，我们希望买来的宝马车所包含的零部件都是同一系列的，以保证最大的匹配度，从而带来更好的性能与舒适度。

所以消费者不愿意到轮胎工厂、方向盘工厂、车窗工厂去一个个采购，而是将需求提给了宝马工厂这家抽象工厂，由这家工厂负责组装。那你是这家工厂的老板，已知汽车的组成部件是固定的，只是不同配件有不同的型号，分别来自不同的制造厂商，你需要推出几款不同组合的车型来满足不同价位的消费者，你会怎么设计？

### 迷宫游戏

你做一款迷宫游戏，已知元素有房间、门、墙，他们之间的组合关系是固定的，你通过一套算法生成随机迷宫，这套算法调用房间、门、墙的工厂生成对应的实例。但随着新资料片的放出，你需要生成具有新功能的房间（可以回复体力）、新功能的门（需要魔法钥匙才能打开）、新功能的墙（可以被炸弹破坏），但修改已有的迷宫生成算法违背了开闭原则（需要在已有对象进行修改），如果你希望生成迷宫的算法完全不感知新材料的存在，你会怎么设计？

### 事件联动

假设我们做一个前端搭建引擎，现在希望做一套关联机制，以实现点击表格组件单元格，可以弹出一个模态框，内部展示一个折线图。已知业务方存在定制表格组件、模态框组件、折线图组件的需求，但组件之间联动关系是确定的，你会怎么设计？

## 意图解释

在汽车工厂的例子中，我们已知车子的构成部件，**为了组装成一辆车子，需要以一定方式拼装部件，而具体用什么部件是需要可拓展的**。

在迷宫游戏的例子中，我们已知迷宫的组成部分是房间、门、墙，**为了生成一个迷宫，需要以某种算法生成许多房间、门、墙的实例，而具体用哪种房间、哪种门、哪种墙是这个算法不关心的，是需要可被拓展的**。

在事件联动的例子中，我们已知这个表格弹出趋势图的交互场景基本组成元素是表格组件、模态框组件、折线图组件，**需要以某种联动机制让这三者间产生联动关系，而具体是什么表格、什么模态框组件、什么折线图组件是这个事件联动所不关心的，是需要可以被拓展的**，表格可以被替换为任意业务方注册的表格，只要满足点击 `onClick` 机制就可以。

> **意图：提供一个接口以创建一系列相关或相互依赖的对象，而无须指定它们具体的类。**

这三个例子不正是符合上面的意图吗？我们要设计的抽象工厂就是要 **创建一系列相关或相互依赖的对象**，在上面的例子中分别是汽车的组成配件、迷宫游戏的素材、事件联动的组件。**而无须指定它们具体的类**，也就说明了我们不关心车子方向盘用的是什么牌子，迷宫的房间是不是普通房间，联动机制的折线图是不是用 `Echarts` 画的，我们只要描述好他们之间的关系即可，**这带来的好处是，未来我们拓展新的方向盘、新的房间、新的折线图时，不需要修改抽象工厂。**

## 结构图

<img width=800 src="https://img.alicdn.com/tfs/TB1k8DVVkT2gK0jSZFkXXcIQFXa-1472-658.png">

`AbstractFactory` 就是我们要的抽象工厂，描述了创建产品的抽象关系，比如描述迷宫如何生成，表格和趋势图怎么联动。

至于具体用什么方向盘、用什么房间，是由 `ConcreteFactory` 实现的，所以我们可能有多个 `ConcreteFactory`，比如 `ConcreteFactory1` 实例化的墙壁是普通墙壁，`ConcreteFactory2` 实例化的墙壁是魔法墙壁，但其对 `AbstractFactory` 的接口是一致的，所以 `AbstractFactory` 不需要关心具体调用的是哪一个工厂。

`AbstractProduct` 是产品抽象类，描述了比如方向盘、墙壁、折线图的创建方法，而 `ConcreteProduct` 是具体实现产品的方法，比如 `ConcreteProduct1` 创建的表格是用 `canvas` 画的，折线图是用 `G2` 画的，而 `ConcreteProduct2` 创建的表格是用 `div` 画的，折线图是用 `Echarts` 画的。

这样，当我们要拓展一个用 `Rcharts` 画的折线图，用 `svg` 画的表格，用 `div` 画的模态框组成的事件机制时，只需要再创建一个 `ConcreteFactory3` 做相应的实现即可，再将这个 `ConcreteFactory3` 传递给 `AbstractFactory`，并不需要修改 `AbstractFactory` 方法本身。

## 代码例子

下面例子使用 javascript 编写。

```typescript
class AbstractFactory {
  createProducts(concreteFactory: ConcreteFactory) {
    const productA = concreteFactory.createProductA();
    const productB = concreteFactory.createProductB();

    // 建立 A 与 B 固定的关联，即便 A 与 B 实现换成任意实现都不受影响
    productA.bind(productB);
  }
}
```

`productA.bind(productB)` 是一种抽象表示：

- 对于汽车工厂的例子，表示组装汽车的过程。
- 对于迷宫游戏的例子，表示生成迷宫的过程。
- 对于事件联动的例子，表示创建组件间关联的过程。

假设我们的迷宫有两套素材，分别是普通素材与魔法素材，只要在分别创建普通素材工厂 `ConcreteFactoryA`，与魔法素材工厂 `ConcreteFactoryB`，调用 `createProducts` 时传入的是普通素材，则产出的就是普通素材搭建的迷宫，传入的是魔法素材，则产出的就是用魔法素材搭建的迷宫。

当我们要创建一套新迷宫材料，比如熔岩迷宫，我们只要创建一套熔岩素材（熔岩房间、熔岩门、熔岩墙壁），再组装一个 `ConcreteFactoryC` 熔岩素材生成工厂传递给 `AbstractFactory.createProducts` 即可。

我们可以发现，使用抽象工厂模式，我们可以轻松拓展新的素材，比如拓展一套新的汽车配件，拓展一套新的迷宫素材，拓展一套新的事件联动组件，**这个过程只需要新建类即可，不需要修改任何类，符合开闭原则**。

## 弊端

任何设计模式都有其适用场景，反过来也说明了在某些场景下不适用。

还是上面的例子，如果我们的需求不是拓展一个新轮子、新墙壁、新折线图，而是：

- 汽车工厂要给汽车加一个新部件：自动驾驶系统。
- 迷宫游戏要新增一个功能素材：陷阱。
- 事件联动要新增一个联动对象：明细趋势统计表格。

你看，这种情况不是为已有元素新增一套实现，而是实现一些新元素，就会非常复杂，因为我们不仅要为所有 `ConcreteFactory` 新增每一个元素，还要修改抽象工厂，以将新元素与旧元素间建立联系，违背了开闭原则。

因此，对于已有元素固定的系统，适合使用抽象工厂，反之不然。

## 总结

抽象工厂对新增已有产品的实现适用，对新增一个产品种类不适用，可以参考结合了例子的下图加深理解：

<img width=800 src="https://img.alicdn.com/tfs/TB1Fbn7Vlr0gK0jSZFnXXbRRXXa-1416-852.png">

拓展一个熔岩素材包是 **增加一种产品风格**，适合使用抽象工厂设计模式；拓展一个陷阱是 **增加一个产品种类**，不适合使用抽象工厂设计模式。为什么呢？看下图：

<img width=800 src="https://img.alicdn.com/tfs/TB12fL8VeL2gK0jSZFmXXc7iXXa-1696-640.png">

创建迷宫这个抽象工厂做的事情，**是把已有的房间、门、墙壁建立关联**，因为操作的是抽象类，所以拓展一套具体实现（熔岩素材包）对这个抽象工厂没有感知，这样做很容易。

但如果新增一个产品种类 - 陷阱，可以看到，抽象工厂必须将陷阱与前三者重新建立关联，这就要修改抽象工厂，不符合开闭原则。同时，如果我们已有素材包 1 ～素材包 999，就需要同时增加 999 个对应的陷阱实现（普通陷阱、魔法陷阱、熔岩陷阱），其工作量会非常大。

因此，只有产品种类稳定时，需要频繁拓展产品风格时才适合用抽象工厂设计模式。

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

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