Design-Pattern-Builder
本篇简单描述生成器模式(Builder)
的结构和特征,并着重讨论为什么要使用生成器模式的思考。
场景
生成器模式是一种创建型模式,目的是创建一个对象。通常创建对象,可以通过new
关键字调用构造函数,并在构造函数中传入需要的参数。这在对象结构简单时十分容易。但当类的成员变量变得众多类型复杂,且某些参数可以不是必输的时候,会变得复杂。在一些语言,例如Python
中,可以通过提供方法的默认参数,避开这个问题,来满足不同场景下的构造函数调用。
1 |
|
Java
不支持方法默认参数,但可以通过函数重载,定义多个不同参数数量,种类组合的构造函数,来适应这种参数非必输时,使用构造函数来创建对象。
1 |
|
但这会出现一些问题。当对象有n
个成员变量需要初始化,那么需要
\(2^n\)
个构造函数。这种指数增长的构造函数显然是不合理的。并且当相邻两个参数为相同类型时,去掉其中任意一个参数会导致方法签名完全一致,编译器无法区分两个方法的区别,会显示编译错误。
1 |
|
又或者不提供所有的构造函数,只提供一个全参的构造函数,如果某个参数无需初始化时,则在构造时将对应参数位置传入null
。
1 |
|
当对象的成员变量较多时,会产生一个参数列表非常长的构造函数调用。程序员需要记住每个位置的参数的含义,并在构造时确保不出错。当然现代IDE
都提供了参数提示的功能,在输入参数时敲入逗号,或者鼠标悬停至参数位置时,会提供参数列表变量名和类型的提示。但超长的构造函数总归是不方便也不优雅的。
至于为什么不通过新建对象后,重复调用set
方法,这部分原因放到后文再来谈。
为了解决上述问题,引入了生成器模式。
最简单的生成器模式
类似于工厂模式,生成器模式也使用了一个独立的类,将对象的创建过程从产品类中抽取出来。生成器模式提供了逐步构建复杂对象的方法。将构造函数的一次性调用,拆解成了对生成器设置方法的多次调用。
这个生成器类可以定义在独立的.java
文件中。也可以定义在产品类中,通常是public static class
的形式。生成器类通常拥有和原产品类相同的,或其中一部分成员变量,用于作为调用产品类构造函数的参数。在生成器类中,提供了设置成员变量的方法,通常这个方法的返回值类型会被设置为生成器类本身。这样生成器类对象,可以链式调用设置生成器中的成员变量,如此可以逐步构建复杂对象,而不是直接调用构造函数本身。最后生成器类还提供了生成方法,调用产品类构造函数,返回产品类对象。在生成方法中,还可以包含一些校验逻辑。
1 |
|
以上便是生成器模式的最简化的结构。究其核心部分,是和产品类相同的成员变量,提供了设置成员变量的设置方法,以及创建对象的生成方法。这样的结构在第三方库中可以经常看到,但这和大部分教材中描述的生成器模式还是有些区别。接下来描述教材中的生成器模式,并描述相对于最简单的生成器模式,多了些什么东西。
复杂些的生成器模式
在上一节中,描述了最简单的生成器结构,创建了一种产品类。对于单个产品类来说,这样子的结构是足够且合适的。
但设计模式考虑代码拓展性,如果多个产品类具有类似的成员变量和构建过程,则这些产品类的构建过程可以被抽象为生成器接口/抽象类。
1 |
|
当多个产品类实现了共同的接口或继承于共同的父类,那么在生成器中可以提供build
生成方法,并返回接口/父类类型。如果没有共同的接口或父类,则build
方法需要在每个具体builder
中单独定义。
定义完生成器接口后,需要针对每一个产品类,定义对应的具体生成器类,这点和工厂方法
模式很像。区别是工厂方法通常只声明了创建抽象产品的方法,而在生成器接口中通常还声明了多个产品共同构建过程的设置方法。
1 |
|
此时,客户端通过实例化具体的生成器类,并调用其中由生成器接口声明的设置方法和生成方法,获取产品类对象。
但此刻,客户端类仍体现出对生成器接口过度的依赖:客户端需要了解生成器接口声明的设置方法。如果多处对生成器类的设置方法调用,具有相似的结构,则可以将这些对于生成器设置方法的调用,抽取到专门的类当中,这个类通常称为主管类(director)
。
1 |
|
至此,整个生成器模式的结构应该和大部分设计模式教材中一致,总结一下其中包含的部分。
- 产品:需要创建的对象,此处不分区抽象/具体产品。
- 生成器接口/抽象生成器:抽象了多种产品构建过程中的相同部分,声明为设置方法。设置方法通常返回生成器接口本身。
- 具体生成器:一个具体生成器对应一个具体产品,实现了生成器接口中声明的设置方法。并提供生成方法创建具体的产品对象。
- 主管类:通过生成器接口,调用了生成器的具体设置方法。
生成器模式的各种变体
由于生成器模式有不同的展现方式,在网上查资料时,虽然各种文章都在描述生成器模式,但感觉每篇文章讲的都不是同一个东西。这可能与原版生成器模式(四人帮提出的23种设计模式)过于复杂,在实践过程中产生了各种简化版本和变体有关。也可能是因为builder
这个概念被滥用,而导致发生了众多歧义。笔者结合自己看过的文章,以及查阅JDK
和各种第三方库中的生成器源代码,来对不同实现做个列举。
- 简单生成器:大部分情况下,产品类不需要拓展,因此也不需要生成器接口,主管类也可以省略。此时只新增了一个生成器类,这个类可以通过静态内部类形式定义在产品类本身代码中。简单生成器是实践中最常见的形式。
- 产品类:多个具体产品类可以实现相同接口,也可以不实现。只要有相似的构建过程,完全不相同的产品类也可以使用相同的生成器接口。这是因为生成器接口只定义了构建过程,而具体实现由具体生成器类实现。
- 生成器接口/抽象生成器:如果生成的产品实现了同一接口,可以像
工厂方法
那样定义生成方法返回产品接口类型。如果没有,则可以让具体生成器定义生成方法的返回类型。此时主管类中也无法通过生成器接口调用生成方法,转由客户端来调用。 - 具体生成器:具体生成器可以拥有和产品类相同的成员变量,也可以用产品作为成员变量。如果以产品作为成员变量,需要在构造时或提供初始化方法新建产品对象,并赋值给成员变量。调用生成方法时将成员变量返回即可。
- 主管类:大部分情况下可以省略,对设置方法的调用通常转由客户端来完成。一方面是因为大部分设置方法的调用是定制的,无法确定每个产品设置方法的调用顺序,次数,以及参数,难以抽取公共方法。另一方面客户端类对生成器接口的依赖是可以被接受的。
为什么使用生成器模式
笔者在学习完生成器模式后,很长一段时间无法体会到其实用性。浏览网上众多资料,给出的原因大多归于这么几类:
- 避免过于复杂的构造函数调用,提供了逐步构建复杂对象的方法
- 链式调用
对于原因1,另一个想当然的解决方法就是调用空参构造函数,并在新建完对象后,反复调用java bean
规范的set
方法,便可以达成逐步构建复杂对象的目的。对于原因2,可以手动将set
方法设置为返回对象本身(这会破坏java bean
规范),便可实现链式调用。
1 |
|
如此来看生成器模式仿佛是多余的,所有的特性都可以通过其他更简便的形式来实现。
会产生这样的想法,是因为上述例子中,将产品的设置方法、生成方法都做了简化。上述生成器中的设置方法,除了返回生成器本身,和java bean
的set
方法无异。而实际情况中,设置方法可能复杂包含逻辑的。例如设置方法会对输入进行校验,可以联动调整多个属性,或者向一个Collection
中反复添加元素。这些功能不是简单的setter
能实现的。如果在产品类中添加太多创建对象的逻辑规则,则又违反了表现和构建相分离的原则(单一职责原则)。
另一个十分重要的原因是,在JDK
和许多第三方库中使用了不可变对象。不可变对象(immutable objects)
是指一旦创建后,其状态就不能被修改的对象。这意味着对象的所有成员变量都是不可变的,它们的值在对象的整个生命周期内保持不变。
不可变对象有许多优秀特性,例如线程安全,无副作用等。不过这不是本篇讨论的重点。要创建一个不可变对象,最简单的是将所有成员变量声明为
private
和final
,并且不提供修改成员变量的公共方法(setter)
。由于成员变量被声明为final
,则必须要在构造函数中进行初始化,否则会报编译错误。不可变对象无法先新建对象,再调用设置方法的形式去为对象赋值。
从这个视角看,生成器替不可变对象暂存了成员变量,并提供了基于成员变量的一些设置方法、校验规则。如果生成器的目的是创建不可变对象,则无法使用产品类作为成员变量的变体。
一个体现了这个原因,最常用的生成器便是StringBuilder
。在java
中,String
类为不可变类,所有的String
对象在实例后就不会再变化,所有涉及字符串操作,都是通过创建新String
对象来实现。例如
1 |
|
如果暂不考虑常量池,这行代码一共存在了三个String
对象,分别为"1"
,"2"
,"12"
。如果每一次字符串操作都会新实例化一个String
,多次的字符串操作所带来的申请、初始化内存操作是性能低下的。String
本身是个字节数组,StringBuilder
通过一个字节数组成员变量,并提供了对字节数组增删改的操作,使得进行字符串操作时,无需每次操作都生成一个String
对象。最后,调用toString()
方法,来将生成器中的数据用来构造String
对象。
此外,生成器模式还体现了一种,维护对象原子性、一致性、有效性的作用。如果采用新建对象再设置的方式,则对象可能处于无效状态。例如
1 |
|
创建了一个长方形对象。对于一个长方形来说,长和宽都是必须的,不存在没有长或者没有宽的长方形。新建对象后再设置的过程中,不可避免对象会处于这种无效的状态。为了避免这种无效的状态,必须要在构造函数中提供所有必须的参数。
而在生成器的生成方法中,可以通过参数校验,来限制这种无效对象的产生,保证产生的不可变对象都是有效的。
总结
生成器模式是一种创建型设计模式,用于将复杂对象的构建过程与其表示分离,以便可以按步骤构建对象,同时隐藏其构建细节。
最常见、最简单的生成器模式只包含一个产品类,一个生成器类,并且通常将生成器类以静态内部类定义在产品类中。
生成器类通过提供设置方法和生成方法,提供了逐步构建复杂对象的方法。
生成器模式可以通过新增抽象产品、抽象生成器、主管类来进行拓展。
笔者认为生成器模式最重要的目的是保证对象的原子性、一致性、有效性。通常和不可变对象一起使用。