设计模式-Builder 模式

创建型模式

生成器/建造者模式( 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的主要代码如下:

java 复制代码
// 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 可以根据需要选择设置不同的字段,创建出不同的对象实例,一些创建示例代码如下:

java 复制代码
// 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的示例代码如下:

java 复制代码
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<T> of(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<T, P> {
        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 模式,以链式的方式构造对象。

相关推荐
Asthenia041226 分钟前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz96544 分钟前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom1 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide1 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9651 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua2 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫