设计模式-Builder

109 阅读5分钟

创建型模式

生成器/建造者模式( Builder Pattern)模式将一个复杂对象的构建(简单理解为设置对象字段值的过程)与它的表示(简单理解为创建对象引用的过程)分离,使得同样的构建过程可以创建不同的表示。

组成

建造者模式的实现需要3个参与者:要建造的对象、建造者及其具体实现、建造者的持有者。

  • 要建造的对象就是Builder要创建的复杂对象;
  • 建造者及其实现就是负责建造对象的Builder接口及其具体实现;
  • 建造者的持有者持有Builder的引用,并调用Builder的方法,建造不同的复杂对象。

建造者模式的组成UML如图1所示:

图1. Builder模式的组成

在图1中,各个组成的作用:

  • Product-产品:要创建的对象
  • Builder/ConcreteBuilderA/ConcreteBuilderB/ConcreteBuilderC-建造者:Builder接口定义创建 Product 对象各个部分的方法,它的实现类提供具体实现
  • Director:持有 Builder 的引用,执行方法来创建 Product 对象

协作过程如下:

  • 客户创建 Director 对象,并用它所想要的 Builder 对象进行配置
  • 一旦要创建 Product,Director 就调用 Builder
  • Builder 创建 Product 的部件,并将部件添加到 Product 中
  • 客户从 Builder 中得到 Product

应用场景

在以下情况时适合使用 Builder模式:

  • 当创建复杂对象时,即被创建的对象有复杂的内部结构,比如有很多字段、字段类型多样等等,不适合使用 构造器 创建和赋值
  • 对象的创建过程独立于它的组成部分,且构造过程可以导致被构造的对象有不同的表示,比如在不同业务场景下创建的对象需要给不同的字段设置值

示例代码

Eurka:InstanceInfo

InstanceInfo 是Eureka 的一个核心类,它记录了 Eureka 实例的所有信息,该有20+的字段属性。InstanceInfo 提供了全参构造器,但是在不同场景下有些字段是必需的有些不是,因此为了方便创建 InstanceInfo 对象,通常使用它的 Builder(它的静态内部类)来构造它的实例对象,Builder的主要代码如下:

// Builder
public static final class Builder {
    @XStreamOmitField
    private InstanceInfo result;

    private Builder(InstanceInfo result, VipAddressResolver vipAddressResolver, Function<String,String> intern) {
        // ...
        this.result = result;
    }
    
    // ... 省略 setXXX 方法..

    /**
    * Build the {@link InstanceInfo} object.
    *
    * @return the {@link InstanceInfo} that was built based on the
    * information supplied.
    */
    public InstanceInfo build() {
        if (!isInitialized()) {
            throw new IllegalStateException("name is required!");
        }
        return result;
    }
}

在使用 Builder 创建 InstanceInfo 对象(Product)时,先创建一个Builder对象,在它的内部有一个 InstanceInfo 成员变量 result,然后调用属性的 set() 方法设置 result 的属性值,最后调用 build() 方法返回result对象。

这样做的好处是,Product 的创建过程(哪些字段需要赋值,哪些不需要)完全由 Director 控制,Director 可以根据需要选择设置不同的字段,创建出不同的对象实例,一些创建示例代码如下:

// ApplicationInfoManager
private void updateInstanceInfo(String newAddress, String newIp) {
   
    InstanceInfo.Builder builder = new InstanceInfo.Builder(instanceInfo);
    if (newAddress != null) {
        builder.setHostName(newAddress);
    }
    if (newIp != null) {
        builder.setIPAddr(newIp);
    }
    builder.setDataCenterInfo(config.getDataCenterInfo());
    instanceInfo.setIsDirty();
}

// InstanceInfoFactory

public InstanceInfo create(EurekaInstanceConfig config) {
    // Builder the instance information to be registered with eureka
    // server
    InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();

    // 省略其他代码...
    builder.setNamespace(namespace).setAppName(config.getAppname())
        .setInstanceId(config.getInstanceId())
        .setAppGroupName(config.getAppGroupName())
        .setDataCenterInfo(config.getDataCenterInfo())
        .setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false))
        .setPort(config.getNonSecurePort())
        .enablePort(InstanceInfo.PortType.UNSECURE,
                    config.isNonSecurePortEnabled())
        .setSecurePort(config.getSecurePort())
        .enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())
        .setVIPAddress(config.getVirtualHostName())
        .setSecureVIPAddress(config.getSecureVirtualHostName())
        .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
        .setStatusPageUrl(config.getStatusPageUrlPath(),
                          config.getStatusPageUrl())
        .setHealthCheckUrls(config.getHealthCheckUrlPath(),
                            config.getHealthCheckUrl(), config.getSecureHealthCheckUrl())
        .setASGName(config.getASGName());

    // 省略... 
    // Add any user-specific metadata information
    for (Map.Entry<String, String> mapEntry : config.getMetadataMap().entrySet()) {
        String key = mapEntry.getKey();
        String value = mapEntry.getValue();
        // only add the metadata if the value is present
        if (value != null && !value.isEmpty()) {
            builder.add(key, value);
        }
    }

    InstanceInfo instanceInfo = builder.build();
    // ... 省略
    return instanceInfo;
}

// EurekaConfigBasedInstanceInfoProvider

public synchronized InstanceInfo get() {
    if (instanceInfo == null) {
        // ...
        InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(vipAddressResolver);

       // ...
        builder.setNamespace(config.getNamespace())
            .setInstanceId(instanceId)
            .setAppName(config.getAppname())
            .setAppGroupName(config.getAppGroupName())
            .setDataCenterInfo(config.getDataCenterInfo())
            .setIPAddr(config.getIpAddress())
            .setHostName(defaultAddress)
            .setPort(config.getNonSecurePort())
            .enablePort(PortType.UNSECURE, config.isNonSecurePortEnabled())
            .setSecurePort(config.getSecurePort())
            .enablePort(PortType.SECURE, config.getSecurePortEnabled())
            .setVIPAddress(config.getVirtualHostName())
            .setSecureVIPAddress(config.getSecureVirtualHostName())
            .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
            .setStatusPageUrl(config.getStatusPageUrlPath(), config.getStatusPageUrl())
            .setASGName(config.getASGName())
            .setHealthCheckUrls(config.getHealthCheckUrlPath(),
                                config.getHealthCheckUrl(), config.getSecureHealthCheckUrl());
  // ...
        instanceInfo = builder.build();
        instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
    }
    return instanceInfo;
}

从 InstanceInfo 的创建过程可以看出,在不同的场景下,Director 可以给不同字段设置值,每一步都是可见可控的,创建出的对象由创建过程控制。

通用Builder

在上面的示例中,Builder能一步一步地创建对象,但是存在一个问题:对每一个复杂对象,我们都要创建一个对应的Builder,再提供一遍类字段的 set() 方法。

为了解决这个问题,有一种“通用Builder”的实现方式。

使用 Java 的泛型、Supplier、Consumer等特性,实现了对不同Product类的通用创建,通用Builder的示例代码如下:

public class Builder<T> {

    private final Supplier<T> instantiator;

    private List<Consumer<T>> modifiers = new ArrayList<>();

    public Builder(Supplier<T> instant) {
        this.instantiator = instant;
    }

    public static <T> Builder<Tof(Supplier<T> instant) {
        return new Builder<>(instant);
    }

    public <P> Builder<T> with(Consumer1<T, P> consumer, P p) {
        Consumer<T> c = instance -> consumer.accept(instance, p);
        modifiers.add(c);
        return this;
    }

    public T build() {
        T value = instantiator.get();
        modifiers.forEach(modifier -> modifier.accept(value));
        modifiers.clear();
        return value;
    }


    /**
     * 自定义 Consumer
     */
    @FunctionalInterface
    public interface Consumer1<TP> {
        void accept(T t, P p);
    }
}

通用Builder在创建对象时,如果要设置某个字段的值,只需要使用 with() 方法,传入对应的 set() 方法的 lambda 表达式和参数,最后使用 build() 方法执行所有方法即可。

优点和缺点

从前面的示例可以得出 Builder 模式的优点和缺点。

优点:

1、可以改变一个产品的内部表示,Director 是调用 Builder 来创建 Product 对象、设置字段值,至于 Product 是如何装配这些字段值,Director是不知道的,这样就方便后续的扩展,可以使用不同的 Builder 来创建满足需要的不同的Product;

2、对构造过程可以进行更加精细的控制,Builder模式在 Director 的控制下,一步一步地构造产品,只有构造完成之后,才从 Builder 中取出(调用 build()方法);

3、将构造代码与表示代码分开,对于复杂的类和对象,用户不需要在创建对象的同时设置它的各个字段值,用户也不需要知道对象内部的所有信息,用户只需要使用Builder提供的方法,按需设置对应的字段值。

缺点:

1、如果 Product 有多个子类,需要对应的 Builder 来创建子类对象。

总结

当不想写大量的 set() 方法时,可以使用 Builder 模式,以链式的方式构造对象。