Java建造者模式详解(附带实例)
所谓建造者(Builder)模式,是把复杂对象的构造过程与主动发起构造操作的代码分隔开,从而能够复用这个构造过程,让我们可以通过不同的参数配置来创建同一个类型的不同对象。
建造者模式很早就出现了,它也是 GoF 设计模式的一种。
建造者模式主要是把复杂实例的构造过程单独提取出来,让主动发起构造操作的那部分代码变得简单一些。把这个构造过程单独划分出来之后,我们还可以将其拆解成多个步骤。这使得用户可以根据自己的需要来安排构造过程中的各个环节,从而以不同的参数构造出同一类型的不同实例。
建造者模式里充当建造者的这部分代码是要单独写成一个类的,这样能方便我们扩充建造者的功能。此模式可以把实例的建造过程封装起来,让这个过程更清晰,也更符合 SOLID 原则。
例如,java.base 模块的 java.lang 包里有 StringBuilder 与 StringBuffer 这样两个类,它们都是建造者类,由于处在 java.lang 包中,因此每一个 Java 应用程序都可以直接使用这两个类,而无须明确引入。
这两个字符串建造者类都提供了各种重载版本的方法,用来接受不同类型的输入值。它们会把这些值与目前已经构造出来的这部分字符序列相拼接,并放置在内部的字节数组里,开发者可以调用 toString 方法,以便在建造完毕之后获取最终建造出来的字符串。
我们还可以举一些例子,例如,java.net.http 包中的 HttpRequest.Builder 接口以及该接口的各种实现类,java.util.stream 包中的 Stream.Builder 接口及其实现类,等等。
建造者模式是一种相当常见的模式,所以 JDK 里有许多地方都用到了这个模式。其中值得一提的是,Locale.Builder 与 Calendar.Builder 这两个建造者类,它们都提供了一系列 setter() 方法,让用户能够在确定最终产品之前先为该产品设定各种参数。这两个类都在 java.base 模块的 java.util 包中。

图 1 怎样用建造者模式方便地制作新的Vehicle实例
建造者模式的职责是让用户能够通过它方便地制作实例,参见如下实例。
【实例】建造者模式里的建造者类可以根据需求采用不同的方式实现。
建造者模式可以用各种方式实现,其中一种是把所有的建造逻辑都封装起来,只提供一个获取成品的建造方法,这样不用暴露任何实现细节,参见下面的实例。
【实例】 VehicleBuilder 类把建造逻辑全都封装了起来,只提供一个方法给用户,令其通过该方法获取建造完成的实例。
另一种方式是让建造者类提供一套方法给用户,令其能够调整正在建造的这个产品(例如,为该产品添加某个部件),并提供一个获取成品的方法,令用户在调整完毕之后通过这个方法获取建造好的产品。在采用这种方式实现的时候,可以把建造者类放在产品类里,参见下面的实例。
【实例】把建造者类设计成产品类的静态嵌套类,用户必须先实例化一个建造者,然后通过它定制产品,定制完毕后,调用建造方法(即 build() 方法)以获取最终产品。
建造者模式使用很广泛,因为它能够减少“代码坏味”(code smell)与构造器污染[是指为了描述各种可定制参数与这些参数的各种使用情况(例如,定制某几个参数并让其他参数保持默认)而设计出许多构造器的做法]。
另外,它还能让代码更易于测试。这个模式让我们不需要再设计那么多种构造器,把每一种定制方式都设计成一个构造器会造成浪费,因为其中有些定制方式可能根本就用不到。
建造者模式还需要考虑是否需要用一个实例来表示建造者:
刚才说的第一种方案不需要这样的实例,用户可以直接通过建造者类的静态方法来制作产品;
第二种方案需要这样的实例,因为正在建造的这件产品必须同一个建造者实例相关联,使得用户可以通过调用该实例的各种方法来调整正在建造的对象。
具体如何选择,要看软件设计者是否允许用户定制正在制作的产品。
即便有了建造者模式,用户通过该模式来建造产品的过程依然比较复杂。所以有的时候可以设法减少这个过程的执行次数,也就是说,如果我们需要创建新的对象,那可以考虑克隆现有的对象。
建造者模式很早就出现了,它也是 GoF 设计模式的一种。
建造者模式主要是把复杂实例的构造过程单独提取出来,让主动发起构造操作的那部分代码变得简单一些。把这个构造过程单独划分出来之后,我们还可以将其拆解成多个步骤。这使得用户可以根据自己的需要来安排构造过程中的各个环节,从而以不同的参数构造出同一类型的不同实例。
建造者模式里充当建造者的这部分代码是要单独写成一个类的,这样能方便我们扩充建造者的功能。此模式可以把实例的建造过程封装起来,让这个过程更清晰,也更符合 SOLID 原则。
建造者模式在JDK中的运用
建造者模式频繁出现在 JDK 里,一个很经典的例子就是用它来创建字符序列(也就是字符串)。例如,java.base 模块的 java.lang 包里有 StringBuilder 与 StringBuffer 这样两个类,它们都是建造者类,由于处在 java.lang 包中,因此每一个 Java 应用程序都可以直接使用这两个类,而无须明确引入。
这两个字符串建造者类都提供了各种重载版本的方法,用来接受不同类型的输入值。它们会把这些值与目前已经构造出来的这部分字符序列相拼接,并放置在内部的字节数组里,开发者可以调用 toString 方法,以便在建造完毕之后获取最终建造出来的字符串。
我们还可以举一些例子,例如,java.net.http 包中的 HttpRequest.Builder 接口以及该接口的各种实现类,java.util.stream 包中的 Stream.Builder 接口及其实现类,等等。
建造者模式是一种相当常见的模式,所以 JDK 里有许多地方都用到了这个模式。其中值得一提的是,Locale.Builder 与 Calendar.Builder 这两个建造者类,它们都提供了一系列 setter() 方法,让用户能够在确定最终产品之前先为该产品设定各种参数。这两个类都在 java.base 模块的 java.util 包中。
建造者模式实例
建造者模式的关键组成部分是充当构建者的这个类,它里面含有建造产品所需的一些值,具体来说,就是一些指向产品部件的引用(参见下图)。
图 1 怎样用建造者模式方便地制作新的Vehicle实例
建造者模式的职责是让用户能够通过它方便地制作实例,参见如下实例。
【实例】建造者模式里的建造者类可以根据需求采用不同的方式实现。
public static void main(String[] args) { System.out.println("Builder pattern: building vehicles"); var slowVehicle = VehicleBuilder.buildSlowVehicle(); var fastVehicle = new FastVehicle.Builder() .addCabin("cabin") .addEngine("Engine") .build(); slowVehicle.parts(); fastVehicle.parts(); }程序输出结果如下:
Builder pattern: building vehicles
SlowVehicle, engine: RecordPart [name=engine]
SlowVehicle, cabin: StandardPart {name='cabin'}
FastVehicle, engine: StandardPart {name='Engine'}
FastVehicle, cabin: RecordPart [name=cabin]
建造者模式可以用各种方式实现,其中一种是把所有的建造逻辑都封装起来,只提供一个获取成品的建造方法,这样不用暴露任何实现细节,参见下面的实例。
【实例】 VehicleBuilder 类把建造逻辑全都封装了起来,只提供一个方法给用户,令其通过该方法获取建造完成的实例。
final class VehicleBuilder { static Vehicle buildSlowCar() { var engine = new RecordPart("engine"); var cabin = new StandardPart("cabin"); return new SlowCar(engine, cabin); } }
另一种方式是让建造者类提供一套方法给用户,令其能够调整正在建造的这个产品(例如,为该产品添加某个部件),并提供一个获取成品的方法,令用户在调整完毕之后通过这个方法获取建造好的产品。在采用这种方式实现的时候,可以把建造者类放在产品类里,参见下面的实例。
【实例】把建造者类设计成产品类的静态嵌套类,用户必须先实例化一个建造者,然后通过它定制产品,定制完毕后,调用建造方法(即 build() 方法)以获取最终产品。
class FastCar implements Vehicle { final static class Builder { private Part engine; private Part cabin; Builder() {} Builder addEngine(String e) {...} Builder addCabin(String c) {...} FastCar build() { return new FastCar(engine, cabin); } } private final Part engine; private final Part cabin; ... @Override public void move() {...} @Override public void parts() {...} }这两种实现方式都符合 SOLID 原则。建造者模式很好地演示了怎样遵循抽象、多态、继承、封装(APIE)原则,设计出易于重构、扩展或验证的解决方案。
总结
建造者模式把建造产品的复杂逻辑与使用该产品的业务逻辑分开,这体现了单一责任原则(SRP)。因为建造者只有唯一的一个职责,也就是建造产品,这样做能够让代码更容易阅读,也能够减少重复,所以它还符合 DRY 原则。建造者模式使用很广泛,因为它能够减少“代码坏味”(code smell)与构造器污染[是指为了描述各种可定制参数与这些参数的各种使用情况(例如,定制某几个参数并让其他参数保持默认)而设计出许多构造器的做法]。
另外,它还能让代码更易于测试。这个模式让我们不需要再设计那么多种构造器,把每一种定制方式都设计成一个构造器会造成浪费,因为其中有些定制方式可能根本就用不到。
建造者模式还需要考虑是否需要用一个实例来表示建造者:
刚才说的第一种方案不需要这样的实例,用户可以直接通过建造者类的静态方法来制作产品;
第二种方案需要这样的实例,因为正在建造的这件产品必须同一个建造者实例相关联,使得用户可以通过调用该实例的各种方法来调整正在建造的对象。
具体如何选择,要看软件设计者是否允许用户定制正在制作的产品。
即便有了建造者模式,用户通过该模式来建造产品的过程依然比较复杂。所以有的时候可以设法减少这个过程的执行次数,也就是说,如果我们需要创建新的对象,那可以考虑克隆现有的对象。