# Builder（生成器）

Builder（生成器）属于创建型模式，针对的是单个复杂对象的创建。

**意图：将一个复杂对象的构建与它的表示分离，使得同样的构建过程可以创建不同的表示。**

## 举例子

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

### 搭乐高积木

乐高积木是很典型的随机拼装场景，你有很多乐高积木，要搭一个小房子都太复杂了，可能不得不看着说明书一步步操作，这就像创建一个复杂的对象，要传入非常多的参数，而且顺序还不能错。

如果不考虑拼装乐高过程中的乐趣，你只是想快速得到一个标准的房子，怎么样才可以最快最省事？

### 工厂流水线

制作一个罐头要经历许多步骤，而其中一些步骤比如制作罐头是通用的，可以用这个罐头装很多东西，比如红枣罐头、黄桃罐头，那工厂流水线是怎么做到灵活可拓展的呢？

### 创建数据库连接池

建立一个数据库连接池，我们需要传入数据库的地址、用户名与密码、还有要创建多少大小的连接池，缓存的位置等等。

考虑到数据库必须正确连接后才有效，创建时必须校验传入的数据库地址与密码的正确性，甚至存储方式与数据库类型还有关系，这是一个简单的 `new` 实例化可以解决的吗？

## 意图解释

在乐高积木的例子中，我们为了得到一个房子其实不需要关心每一个积木应该如何摆放，**我们只要交给组装工厂（一个人或者一个程序）产出标准房子就行了**，这其中参数可能是 `.setHouseType().build()` 设置房屋类型，而不需要 `new House(block1, block2, ... block999)` 传递这些没必要的参数。**其中组装工厂就是生成器**。

在工厂流水线的例子中，**流水线就是生成器，一个流水线可以不通过不同组合生成不同作用的工厂**，黄桃罐头的流水线可以理解为 `new Builder().组装罐头().放入黄桃().build()`，红枣罐头的流水线可以理解为 `new Builder().组装罐头().放入红枣().build()`，我们可以复用生成器最基础的函数 `组装罐头()` 将其用于创建不同的产品中，复用了组装基础能力。

在创建数据库例子中，我们可以先设置一些必要的参数再创建，比如 `new Builder().setUrl().setPassword().setType().build()`，这样在最终执行 `build` 函数的时候，可以对参数中存在关联的进行校验，而得到的对象也无法再被修改，这样比直接暴露数据库连接池对象，再一个值一个值 Set 多了如下好处：

1. 对象无法被修改，保护了程序稳定性，减少了维护复杂度。
2. 可以对参数关联进行一次性校验。
3. 在创建对象之前不会存在中间态，即创建了对象实例，但缺少部分参数，这可能导致对象无法正确 work。

**意图：将一个复杂对象的构建与它的表示分离，使得同样的构建过程可以创建不同的表示。**

我们再理解一次意图，所谓构建与表示分离，就是指一个对象 `Person` 并不是简单的 `new Person()` 就可以实例化出来的，如果可以，那就是构建与表示一体。**所谓构建与表示分离，就是指 `Person` 只能描述，而不能通过 `new Person()` 实例化，将实例化工作通过 Builder 实现，这样同样一个构建过程可以创建不同的 `Person` 实例。**

在乐高积木的例子中，通过乐高创建的房子并不是 `new House()` 出来，而是将构建与表示分离了，工厂流水线中我们创建一个黄桃罐头，不是通过 `new 黄桃罐头()`，而是通过流水线不同拼装方式来完成，在数据库例子中，我们没有通过 `new DB()` 的方式创建数据库，而是通过 Builder 来创建，这都体现了构建与表示的分离。

## 结构图

<img width=800 src="https://img.alicdn.com/tfs/TB14lOwYXT7gK0jSZFpXXaTkpXa-1382-466.png">

- `Director` 指导器，用来指导构建过程。
- `Builder` 生成器接口，用来提供一系列构建对象的方法，以及最终的 `build` 生成对象函数，这个函数里可以做一些参数校验。
- `ConcreteBuilder` 是 `Builder` 的具体实现。

实际上，Builder 模式抽象层次可高可低，我们上面三个例子都没有用到指导器与生成器接口，这是因为在代码不太复杂的情况下，可以使用简化模型。

## 代码例子

下面例子使用 javascript 编写。

```typescript
class Director {
  create(concreteBuilder: ConcreteBuilder) {
    // 创建了一些零件
    concreteBuilder.buildA();
    concreteBuilder.buildB();

    // 校验参数已经生成实例
    return concreteBuilder.build();
  }
}

class HouseBuilder {
  public buildA() {
    // 创建房屋
    // this.xxx = xxx
  }

  public buildB() {
    // 刷油漆
  }

  public build() {
    // 最终创建实例
    return new House(/* ..一堆参数 this.xxx.. */);
  }
}

// 接下来是正式使用
const director = new Director();
const builder = HouseBuilder();
const house = director.create(builder);
```

上面的例子是完整版本的 Builder 模式，抽象了指导器 `Director` 与生成器 `Builder`，只要两者都严格按照接口实现，我们可以：

1. 替换任意 `Director`，使创建的过程做任意修改。
2. 替换任意 `Builder`，使创建的实现做任意修改。

做了任意的改动，都可以得到不同的房子实现，这就是创建与表示分离的好处，我们可以通过同样的构建过程创建不同的表示。

这个 `director.create()`：

- 在搭乐高积木的例子，表示用乐高搭建房屋的过程。
- 在工程流水线的例子，表示罐头的组装构成。
- 在创建数据库连接池的例子，表示数据库连接池的创建过程。

而 `Builder` 以及其函数 `buildA` `buildB` 等方法表示具体制造方法，比如：

- 在搭乐高积木的例子，表示如何盖房子，如何刷油漆。
- 在工程流水线的例子，表示如何做一个罐头，如何添加黄桃。
- 在创建数据库连接池的例子，表示如何设置数据库地址，如何设置用户名密码等。

对于数据库的例子中，我们不仅可以保证创建对象的便捷性，因为不需要传入过多参数，也保证了对象的正确校验，同时生成的实例也是不可变的。

更重要的是，如果使用完整模式，我们可以替换 `Director` 来修改创建数据库的方式，替换 `Builder` 来修改具体方法，比如 `.setUserName` 这个函数不做具体实现，而是统计性能，`build()` 函数创建的不是一个数据库连接实例，而是一个测试实例。

再比如前端同一个方法在 JS 和 Node 环境下运行效果不一样，我们可以实现 `BrowserBuild` 与 `NodeBuild`，实现相同的接口，这样可以共享相同的创建过程，创建不同环境可以运行的实例。

可以看到，使用 Builder 模式可以保证创建对象的便捷与稳定性，还留了足够的拓展空间改变对象的创建过程与创建方法，具有极强的拓展性。

## 弊端

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

- 实例化对象非常繁琐，重复定义了许多对象成员变量的 `set` 方法，而且也不如 `new` 看的直观，也就是场景足够简单时，不需要任何地方都用 Builder 实例化对象。
- 一个对象只有一种表示时，没必要做如此地步的抽象。

上面的例子都是相对复杂的，假设我们的搭房子的例子中，我们不是用乐高积木搭建，而是用两块半成品模板拼起来就得到一个房子，那就没有必要使用 Builder 模式，直接 `new House()` 即可。

再者，如果我们只需要生产各种罐头，而不需要生产汽车，那么就没必要过度抽象 Builder，把创建汽车的方法也囊括进去，最后，如果我们的对象只有一种表示时，没有必要抽象 Builder，也就是流水线如果只生产黄桃罐头，就没必要把各个生产环节变成可拆卸的，因为也没有重新组合的需要。

## 总结

Builder 模式对于创建一个复杂对象特别有用，可以看下图加深理解：

<img wdith=800 src="https://img.alicdn.com/tfs/TB109aLYoT1gK0jSZFrXXcNCXXa-1412-984.png">

最后总结一下何时适合用 Builder 模式：只有当创建过程允许被构造对象有不同表示，或者对象复杂到对象描述与创建对象过程值得分离时，才使用 Builder 设计模式。

> 讨论地址是：[精读《设计模式 - Builder 生成器》· Issue #273 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/273)

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