创建型模式

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

组成

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

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

建造者模式的组成UML如图1所示(来自参考文献[1]):
image.png
图1. Builder模式的组成
在图1中,各个组成的作用:

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

协作过程如下:

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

    应用场景

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

  • 当创建复杂对象时,即被创建的对象有复杂的内部结构,比如有很多字段、字段类型多样等等,不适合使用 构造器 创建和赋值

  • 对象的创建过程独立于它的组成部分,且构造过程可以导致被构造的对象有不同的表示,比如在不同场景下创建的对象需要给不同的字段设置值

参考文献[2]的“建造者模式”部分给出了一个Builder模式的案例,它的案例UML如图2所示:
image.png
图2. 参考文献[2]的 Builder 案例
在图2中,BuilderPatternDemo就是前面所说的 Director,MealBuilder 就是 Builder,Product 包含了比较多的一些类(不需要关注具体的类,只需要知道它们共同构成了 Product);Director 调用 Builder 创建 Product 的代码如下(来自参考文献[2]):

  1. // Director
  2. public class BuilderPatternDemo {
  3. public static void main(String[] args) {
  4. MealBuilder mealBuilder = new MealBuilder();
  5. // Product1
  6. Meal vegMeal = mealBuilder.prepareVegMeal();
  7. System.out.println("Veg Meal");
  8. vegMeal.showItems();
  9. System.out.println("Total Cost: " +vegMeal.getCost());
  10. // Product2
  11. Meal nonVegMeal = mealBuilder.prepareNonVegMeal();
  12. System.out.println("\n\nNon-Veg Meal");
  13. nonVegMeal.showItems();
  14. System.out.println("Total Cost: " +nonVegMeal.getCost());
  15. }
  16. }
  17. // Builder
  18. public class MealBuilder {
  19. // Builder1
  20. public Meal prepareVegMeal (){
  21. Meal meal = new Meal();
  22. meal.addItem(new VegBurger());
  23. meal.addItem(new Coke());
  24. return meal;
  25. }
  26. // Builder2
  27. public Meal prepareNonVegMeal (){
  28. Meal meal = new Meal();
  29. meal.addItem(new ChickenBurger());
  30. meal.addItem(new Pepsi());
  31. return meal;
  32. }
  33. }

比较图1和图2以及案例代码,有以不同和问题:

  • 不同:没有 ConcreteBuilder,只有一个 Builder
  • 问题1:不同的 Product 由 Builder 的不同方法创建,如果我们要创建新的 Product,就需要来修改 Builder,不符合“开闭原则”
  • 问题2:Builder 的方法直接创建了 Product,创建的步骤不由 Director 控制,无法更改创建步骤

更好的实现方式是:不同的 ConcreteBuilder 创建不同的 Product,并且能够由 Director 来控制每一个字段(部件)的设置。

示例代码

Eurka:InstanceInfo

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

  1. // Builder
  2. public static final class Builder {
  3. @XStreamOmitField
  4. private InstanceInfo result;
  5. private Builder(InstanceInfo result, VipAddressResolver vipAddressResolver, Function<String,String> intern) {
  6. // ...
  7. this.result = result;
  8. }
  9. // ... 省略 setXXX 方法..
  10. /**
  11. * Build the {@link InstanceInfo} object.
  12. *
  13. * @return the {@link InstanceInfo} that was built based on the
  14. * information supplied.
  15. */
  16. public InstanceInfo build() {
  17. if (!isInitialized()) {
  18. throw new IllegalStateException("name is required!");
  19. }
  20. return result;
  21. }
  22. }

在使用 Builder 创建 InstanceInfo 对象(Product)时,先创建一个Builder对象,在它的内部有一个 InstanceInfo 成员变量 result,然后调用属性的 set() 方法设置 result 的属性值,最后调用 build() 方法返回result对象。
这样做的好处是,Product 的创建过程(哪些字段需要赋值,哪些不需要)完全由 Director 控制,不再像前面(参考文献[2])案例那样,由Builder直接返回创建好的 Product,一些创建示例代码如下:

  1. // ApplicationInfoManager
  2. private void updateInstanceInfo(String newAddress, String newIp) {
  3. InstanceInfo.Builder builder = new InstanceInfo.Builder(instanceInfo);
  4. if (newAddress != null) {
  5. builder.setHostName(newAddress);
  6. }
  7. if (newIp != null) {
  8. builder.setIPAddr(newIp);
  9. }
  10. builder.setDataCenterInfo(config.getDataCenterInfo());
  11. instanceInfo.setIsDirty();
  12. }
  13. // InstanceInfoFactory
  14. public InstanceInfo create(EurekaInstanceConfig config) {
  15. // Builder the instance information to be registered with eureka
  16. // server
  17. InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();
  18. // 省略其他代码...
  19. builder.setNamespace(namespace).setAppName(config.getAppname())
  20. .setInstanceId(config.getInstanceId())
  21. .setAppGroupName(config.getAppGroupName())
  22. .setDataCenterInfo(config.getDataCenterInfo())
  23. .setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false))
  24. .setPort(config.getNonSecurePort())
  25. .enablePort(InstanceInfo.PortType.UNSECURE,
  26. config.isNonSecurePortEnabled())
  27. .setSecurePort(config.getSecurePort())
  28. .enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())
  29. .setVIPAddress(config.getVirtualHostName())
  30. .setSecureVIPAddress(config.getSecureVirtualHostName())
  31. .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
  32. .setStatusPageUrl(config.getStatusPageUrlPath(),
  33. config.getStatusPageUrl())
  34. .setHealthCheckUrls(config.getHealthCheckUrlPath(),
  35. config.getHealthCheckUrl(), config.getSecureHealthCheckUrl())
  36. .setASGName(config.getASGName());
  37. // 省略...
  38. // Add any user-specific metadata information
  39. for (Map.Entry<String, String> mapEntry : config.getMetadataMap().entrySet()) {
  40. String key = mapEntry.getKey();
  41. String value = mapEntry.getValue();
  42. // only add the metadata if the value is present
  43. if (value != null && !value.isEmpty()) {
  44. builder.add(key, value);
  45. }
  46. }
  47. InstanceInfo instanceInfo = builder.build();
  48. // ... 省略
  49. return instanceInfo;
  50. }
  51. // EurekaConfigBasedInstanceInfoProvider
  52. public synchronized InstanceInfo get() {
  53. if (instanceInfo == null) {
  54. // ...
  55. InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(vipAddressResolver);
  56. // ...
  57. builder.setNamespace(config.getNamespace())
  58. .setInstanceId(instanceId)
  59. .setAppName(config.getAppname())
  60. .setAppGroupName(config.getAppGroupName())
  61. .setDataCenterInfo(config.getDataCenterInfo())
  62. .setIPAddr(config.getIpAddress())
  63. .setHostName(defaultAddress)
  64. .setPort(config.getNonSecurePort())
  65. .enablePort(PortType.UNSECURE, config.isNonSecurePortEnabled())
  66. .setSecurePort(config.getSecurePort())
  67. .enablePort(PortType.SECURE, config.getSecurePortEnabled())
  68. .setVIPAddress(config.getVirtualHostName())
  69. .setSecureVIPAddress(config.getSecureVirtualHostName())
  70. .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
  71. .setStatusPageUrl(config.getStatusPageUrlPath(), config.getStatusPageUrl())
  72. .setASGName(config.getASGName())
  73. .setHealthCheckUrls(config.getHealthCheckUrlPath(),
  74. config.getHealthCheckUrl(), config.getSecureHealthCheckUrl());
  75. // ...
  76. instanceInfo = builder.build();
  77. instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
  78. }
  79. return instanceInfo;
  80. }

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

通用Builder

在上面的示例中,Builder能一步一步地创建对象,但是存在一个问题:对每一个复杂对象,我们都要创建一个对应的Builder,再提供一遍类字段的 set() 方法。
为了解决这个问题,网上有一种“Java8通用Builder”(参考文献[3])的实现方式,使用 Java 的泛型、Supplier、Consumer等特性,实现了对不同Product类的通用创建,通用Builder的示例代码如下(来自参考文献[3]):

  1. public class Builder<T> {
  2. private final Supplier<T> instantiator;
  3. private List<Consumer<T>> modifiers = new ArrayList<>();
  4. public Builder(Supplier<T> instant) {
  5. this.instantiator = instant;
  6. }
  7. public static <T> Builder<T> of(Supplier<T> instant) {
  8. return new Builder<>(instant);
  9. }
  10. public <P1> Builder<T> with(Consumer1<T, P> consumer, P p) {
  11. Consumer<T> c = instance -> consumer.accept(instance, p);
  12. modifiers.add(c);
  13. return this;
  14. }
  15. public T build() {
  16. T value = instantiator.get();
  17. modifiers.forEach(modifier -> modifier.accept(value));
  18. modifiers.clear();
  19. return value;
  20. }
  21. /**
  22. * 自定义 Consumer
  23. */
  24. @FunctionalInterface
  25. public interface Consumer1<T, P> {
  26. void accept(T t, P p);
  27. }
  28. }

通用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 模式,以链式的方式构造对象。

附录. 参考文献

  1. Erich Gamma, Richard Helm, Ralph Johnson, Jojn Vlissides. 设计模式—可复用面向对象软件的基础[M]. 机械工业出版社,北京. 2018.11
  2. 软件开发设计模式
  3. Java8通用Builder