首页 > 编程笔记 > Java笔记 阅读:15

Java建造者模式详解(附带实例)

所谓建造者(Builder)模式,是把复杂对象的构造过程与主动发起构造操作的代码分隔开,从而能够复用这个构造过程,让我们可以通过不同的参数配置来创建同一个类型的不同对象。

建造者模式很早就出现了,它也是 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)与构造器污染[是指为了描述各种可定制参数与这些参数的各种使用情况(例如,定制某几个参数并让其他参数保持默认)而设计出许多构造器的做法]。

另外,它还能让代码更易于测试。这个模式让我们不需要再设计那么多种构造器,把每一种定制方式都设计成一个构造器会造成浪费,因为其中有些定制方式可能根本就用不到。

建造者模式还需要考虑是否需要用一个实例来表示建造者:
刚才说的第一种方案不需要这样的实例,用户可以直接通过建造者类的静态方法来制作产品;
第二种方案需要这样的实例,因为正在建造的这件产品必须同一个建造者实例相关联,使得用户可以通过调用该实例的各种方法来调整正在建造的对象。

具体如何选择,要看软件设计者是否允许用户定制正在制作的产品。

即便有了建造者模式,用户通过该模式来建造产品的过程依然比较复杂。所以有的时候可以设法减少这个过程的执行次数,也就是说,如果我们需要创建新的对象,那可以考虑克隆现有的对象。

相关文章