精读《设计模式 - Builder 生成器》
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。
意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
我们再理解一次意图,所谓构建与表示分离,就是指一个对象 Person
并不是简单的 new Person()
就可以实例化出来的,如果可以,那就是构建与表示一体。所谓构建与表示分离,就是指 Person
只能描述,而不能通过 new Person()
实例化,将实例化工作通过 Builder 实现,这样同样一个构建过程可以创建不同的 Person
实例。
在乐高积木的例子中,通过乐高创建的房子并不是 new House()
出来,而是将构建与表示分离了,工厂流水线中我们创建一个黄桃罐头,不是通过 new 黄桃罐头()
,而是通过流水线不同拼装方式来完成,在数据库例子中,我们没有通过 new DB()
的方式创建数据库,而是通过 Builder 来创建,这都体现了构建与表示的分离。
结构图
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 模式对于创建一个复杂对象特别有用,可以看下图加深理解:
最后总结一下何时适合用 Builder 模式:只有当创建过程允许被构造对象有不同表示,或者对象复杂到对象描述与创建对象过程值得分离时,才使用 Builder 设计模式。
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)
发表评论 (审核通过后显示评论):