设计模式(四)建造者模式 Builder
- Builder:
《Effective Java》 第2条:遇到多个构造器参数时要考虑用构建器。
建造者模式(Builder Pattern),也称生成器模式,定义如下:
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
Separate the construction of a complex object from its representation so that the same construction process can create different representation.
- 什么时候使用 Builder:
我们常常会看见这种情况:我需要一个初始化一个复杂对象,在初始化的同时完成参数的赋值工作。
- 数据模型:
@Data @EqualsAndHashCode @ToString public class PolicyCommon { private String code; private String category; private String name; private String inceptionDate; public PolicyCommon() { super(); } public PolicyCommon(String code, String category, String name) { super(); this.code = code; this.category = category; this.name = name; } }
- 模型分析:
PolicyCommon 带有4个参数,假定只有 inceptionDate不需要在创建的时候赋值。
剩余3个参数参与初始化的工作,那么这个对象初始化之后的状态可能性为:2^3=8。
- 传统的解决方案1——使用无参构造器,然后依次调用 set() 方法:
@Test void testPolicyCommon1() { PolicyCommon policy = new PolicyCommon(); policy.setName("Gerrard"); policy.setCode("11123768"); policy.setCategory("Engineer"); System.out.println(policy); }
这种方法,将构造对象的过程拆分成多个动作,是存在风险的。
因为对象不是一次性构造完成,使得对象在构造过程中存在状态不一致的情况。
期间有 this 指针溢出的风险,阻碍了这个对象称为不可变对象的可能。在多线程条件下,需要额外的工作才能保证线程安全。
- 传统的解决方案2——使用重叠参数的构造器:
@Test void testPolicyCommon2() { PolicyCommon policy = new PolicyCommon("11123768", "Engineer", "Gerrard"); System.out.println(policy); }
这样做的劣势在于:
- 从调用者的角度来说,属性的意义不明显,很容易将参数位置颠倒了,但是编译器并没有报错,例如: new PolicyCommon("Engineer", "Gerrard", "11123768");
- 如果我只想初始化一个属性,如 code,那么构造器的调用会有许多冗余参数。例如:new PolicyCommon("11123768", null, null);
- Builder 给出的解决方案:
使用一个静态内部类 Builder,Builder 持有需要动态生成的属性。
Builder 为每一个属性,提供 set() 方法,但是与通常的set() 方法不同,这些 set() 方法的返回值是 Builder 本身。
Builder 提供一个 build() 方法,返回类型为外部类。
外部类中,提供一个参数为 Builder 对象的构造器,且将其的权限设置为 private,如此一来,构造器的唯一途径就是 build() 方法。
- 代码:
@Data @EqualsAndHashCode @ToString public class PolicyBuilder { private String code; private String category; private String name; private String inceptionDate; private PolicyBuilder(Builder builder) { code = builder.code; category = builder.category; name = builder.name; } public static class Builder { private String code; private String category; private String name; public Builder setCode(String code) { this.code = code; return this; } public Builder setCategory(String category) { this.category = category; return this; } public Builder setName(String name) { this.name = name; return this; } public PolicyBuilder build() { return new PolicyBuilder(this); } } }
- 调用 Builder:
@Test void testPolicyBuilder() { PolicyBuilder policy = new PolicyBuilder.Builder().setName("Gerrard").setCode("11123768").build(); System.out.println(policy); }
可以看出,Builder 初始化的过程中,对参数选择更加自由,而且它不会使对象处于不同的状态。
劣势在于冗长的内部类代码,以及构建过程中会增加一个 Builder 对象。
- 使用 Lombok 一键 Builder:
细心的朋友,在最初的 PolicyCommon 的数据模型中,会发现三个注解,这是 Lombok 框架的功能(org.projectlombok,License = MIT)。
@Data,为类的每一个属性,自动生成 get() 和 set() 方法。
@EqualsAndHashCode,为类生成 equals() 和 hashCode() 方法。
@ToString,为类生成 toString() 方法。
除此之外,Lombok 还提供了 @Builder 的注解,自动为类提供了 Builder 的功能。
- Lombok Builder:
@Data @EqualsAndHashCode @ToString public class PolicyBuilderLombok { private String code; private String category; private String name; private String inceptionDate; @Builder public PolicyBuilderLombok(String code, String category, String name) { super(); this.code = code; this.category = category; this.name = name; } }
@Test void testPolicyBuilderLombok() { PolicyBuilderLombok policy = new PolicyBuilderLombok.PolicyBuilderLombokBuilder() .name("Gerrard").code("11123768").build(); System.out.println(policy); }
- 解析 @Builder:
检查元注解:@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
这表明这个注解可以加在:类、构造器、方法(不推荐加在方法上)。
@Builder 加在类上 <==> ,在类的全参构造器上加上 @Builder。
@Builder 加在构造器上,这个构造器的入参就是内部类 Builder 的动态参数。