Builder(生成器)
Builder(生成器)属于创建型模式,针对的是单个复杂对象的创建。
意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
搭乐高积木
乐高积木是很典型的随机拼装场景,你有很多乐高积木,要搭一个小房子都太复杂了,可能不得不看着说明书一步步操作,这就像创建一个复杂的对象,要传入非常多的参数,而且顺序还不能错。
如果不考虑拼装乐高过程中的乐趣,你只是想快速得到一个标准的房子,怎么样才可以最快最省事?
工厂流水线
制作一个罐头要经历许多步骤,而其中一些步骤比如制作罐头是通用的,可以用这个罐头装很多东西,比如红枣罐头、黄桃罐头,那工厂流水线是怎么做到灵活可拓展的呢?
创建数据库连接池
建立一个数据库连接池,我们需要传入数据库的地址、用户名与密码、还有要创建多少大小的连接池,缓存的位置等等。
考虑到数据库必须正确连接后才有效,创建时必须校验传入的数据库地址与密码的正确性,甚至存储方式与数据库类型还有关系,这是一个简单的 new
实例化可以解决的吗?
意图解释
在乐高积木的例子中,我们为了得到一个房子其实不需要关心每一个积木应该如何摆放,我们只要交给组装工厂(一个人或者一个程序)产出标准房子就行了,这其中参数可能是 .setHouseType().build()
设置房屋类型,而不需要 new House(block1, block2, ... block999)
传递这些没必要的参数。其中组装工厂就是生成器。
在工厂流水线的例子中,流水线就是生成器,一个流水线可以不通过不同组合生成不同作用的工厂,黄桃罐头的流水线可以理解为 new Builder().组装罐头().放入黄桃().build()
,红枣罐头的流水线可以理解为 new Builder().组装罐头().放入红枣().build()
,我们可以复用生成器最基础的函数 组装罐头()
将其用于创建不同的产品中,复用了组装基础能力。
在创建数据库例子中,我们可以先设置一些必要的参数再创建,比如 new Builder().setUrl().setPassword().setType().build()
,这样在最终执行 build
函数的时候,可以对参数中存在关联的进行校验,而得到的对象也无法再被修改,这样比直接暴露数据库连接池对象,再一个值一个值 Set 多了如下好处:
对象无法被修改,保护了程序稳定性,减少了维护复杂度。
可以对参数关联进行一次性校验。
在创建对象之前不会存在中间态,即创建了对象实例,但缺少部分参数,这可能导致对象无法正确 work。
意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
我们再理解一次意图,所谓构建与表示分离,就是指一个对象 Persion
并不是简单的 new Persion()
就可以实例化出来的,如果可以,那就是构建与表示一体。所谓构建与表示分离,就是指 Persion
只能描述,而不能通过 new Persion()
实例化,将实例化工作通过 Builder 实现,这样同样一个构建过程可以创建不同的 Persion
实例。
在乐高积木的例子中,通过乐高创建的房子并不是 new House()
出来,而是将构建与表示分离了,工厂流水线中我们创建一个黄桃罐头,不是通过 new 黄桃罐头()
,而是通过流水线不同拼装方式来完成,在数据库例子中,我们没有通过 new DB()
的方式创建数据库,而是通过 Builder 来创建,这都体现了构建与表示的分离。
结构图
![](https://i-blog.csdnimg.cn/blog_migrate/72b5455fc2e2aa81033379c967e8471b.png)
Director
指导器,用来指导构建过程。Builder
生成器接口,用来提供一系列构建对象的方法,以及最终的build
生成对象函数,这个函数里可以做一些参数校验。ConcreteBuilder
是Builder
的具体实现。
实际上,Builder 模式抽象层次可高可低,我们上面三个例子都没有用到指导器与生成器接口,这是因为在代码不太复杂的情况下,可以使用简化模型。
代码例子
下面例子使用 javascript 编写。
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
,只要两者都严格按照接口实现,我们可以:
替换任意
Director
,使创建的过程做任意修改。替换任意
Builder
,使创建的实现做任意修改。
做了任意的改动,都可以得到不同的房子实现,这就是创建与表示分离的好处,我们可以通过同样的构建过程创建不同的表示。
这个 director.create()
:
在搭乐高积木的例子,表示用乐高搭建房屋的过程。
在工程流水线的例子,表示罐头的组装构成。
在创建数据库连接池的例子,表示数据库连接池的创建过程。
而 Builder
以及其函数 buildA
buildB
等方法表示具体制造方法,比如:
在搭乐高积木的例子,表示如何盖房子,如何刷油漆。
在工程流水线的例子,表示如何做一个罐头,如何添加黄桃。
在创建数据库连接池的例子,表示如何设置数据库地址,如何设置用户名密码等。
对于数据库的例子中,我们不仅可以保证创建对象的便捷性,因为不需要传入过多参数,也保证了对象的正确校验,同时生成的实例也是不可变的。
更重要的是,如果使用完整模式,我们可以替换 Director
来修改创建数据库的方式,替换 Builder
来修改具体方法,比如 .setUserName
这个函数不做具体实现,而是统计性能,build()
函数创建的不是一个数据库连接实例,而是一个测试实例。
再比如前端同一个方法在 JS 和 Node 环境下运行效果不一样,我们可以实现 BrowserBuild
与 NodeBuild
,实现相同的接口,这样可以共享相同的创建过程,创建不同环境可以运行的实例。
可以看到,使用 Builder 模式可以保证创建对象的便捷与稳定性,还留了足够的拓展空间改变对象的创建过程与创建方法,具有极强的拓展性。
弊端
任何设计模式都有其适用场景,反过来也说明了在某些场景下不适用。
实例化对象非常繁琐,重复定义了许多对象成员变量的
set
方法,而且也不如new
看的直观,也就是场景足够简单时,不需要任何地方都用 Builder 实例化对象。一个对象只有一种表示时,没必要做如此地步的抽象。
上面的例子都是相对复杂的,假设我们的搭房子的例子中,我们不是用乐高积木搭建,而是用两块半成品模板拼起来就得到一个房子,那就没有必要使用 Builder 模式,直接 new House()
即可。
再者,如果我们只需要生产各种罐头,而不需要生产汽车,那么就没必要过度抽象 Builder,把创建汽车的方法也囊括进去,最后,如果我们的对象只有一种表示时,没有必要抽象 Builder,也就是流水线如果只生产黄桃罐头,就没必要把各个生产环节变成可拆卸的,因为也没有重新组合的需要。
总结
Builder 模式对于创建一个复杂对象特别有用,可以看下图加深理解:
![](https://i-blog.csdnimg.cn/blog_migrate/c5b8393671a4b740ece4d22646014142.png)
最后总结一下何时适合用 Builder 模式:只有当创建过程允许被构造对象有不同表示,或者对象复杂到对象描述与创建对象过程值得分离时,才使用 Builder 设计模式。
讨论地址是:精读《设计模式 - Builder 生成器》· Issue #273 · dt-fe/weekly
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
![](https://i-blog.csdnimg.cn/blog_migrate/13b3f6ec81058e6690894b032a9b2141.png)
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)