java:其实你并不会用Builder
生成器模式在Java应用程序中非常流行。但它经常被大家误解和错误地使用,从而导致运行时错误。
让我们记住使用Builder的目的:仅在某些对象中设置必要的字段,并将其余字段设置为默认值。例如,如果我们正在准备配置对象,那么仅更改必要的参数并将其他参数设置为默认值会很方便。
而当Builder是反模式时
许多开发人员只选择了Builder模式的一部分:可以单独设置字段。第二部分:其余字段存在合理的默认值-通常会被忽略。
结果,很容易获得不完整的(部分初始化的)POJO。为了缓解此问题,我们对build()方法进行了检查,最后可能自己都没发现啥问题但就是出错。此时此刻,主要的损害已经造成:检查转移到了运行时间上。为确保一切正常,我们需要添加专用测试以覆盖创建POJO的代码中的所有执行路径。
如何修复POJO的生成器?
首先,让我们定义目标。这里的目标是将检查返回到编译时。如果未构建完整POJO的代码无法通过编译,则将不需要专用测试,也无需在build()方法中执行检查。但最重要的是,平时我们工作起来就更轻松。有更多时间摸鱼了。
那么,如何做到这一点呢?最明显的方法是使用Fluent API模式。Fluent API有两个部分(顺便说一句,就像Builder一样):提供一种方便的方式来调用链中的方法(这两个部分Fluent API和Builder都相同),并将链中的每个后续调用都限制为仅允许的方法集。
第二部分是我们所要说的部分。通过限制在构建POJO的每个步骤中可以调用的方法集,我们可以强制执行特定的调用序列,并build()仅在设置了所有字段时才启用对方法的调用。这样,我们将所有检查移回编译时间。作为方便的副作用,还确保构建特定POJO的所有位置看起来都相同。这样,发现错误传递的参数或比较代码修订之间的更改将更加容易。
为了区分传统的Builder和带有Fluent API的Builder,我将后者称为Fluent Builder。
假设我们要为如下所示的简单bean创建Fluent Builder:
public class SimpleBean {
private final int index;
private final String name;
public SimpleBean(final int index, final String name) {
this.index = index;
this.name = name;
}
public int index() {
return index;
}
public String name() {
return name;
}
}
在此示例中,我使用了Java 记录获取器的名称约定。在Java 14中,此类可以声明为记录,因此必要的样板代码将大大减少。
让我们添加一个构建器。第一步很传统:
...
public static SimpleBeanBuilder builder() {
return new SimpleBeanBuilder();
}
...
让我们首先实现一个传统的生成器,这样会更加清楚Fluent Builder代码是如何派生的。传统的Builder类如下所示:
...
private static class SimpleBeanBuilder {
private int index;
private String name;
public SimpleBeanBuilder setIndex(final int index) {
this.index = index;
return this;
}
public SimpleBeanBuilder setName(final String name) {
this.name = name;
return this;
}
public SimpleBean build() {
return new SimpleBean(index, name);
}
}
...
一个重要的观察:每个setter返回此值,这又允许此调用的用户调用builder中可用的每个方法。这是问题的根源,因为build()在设置所有必要字段之前,允许用户过早调用该方法。
为了制作Fluent Builder,我们需要将可能的选择限制为仅允许的选择,因此必须正确使用Builder。由于我们正在考虑需要设置所有字段的情况,因此在每个构建步骤中,只有一种方法可用。为此,我们可以返回专用接口,而不是this让Builder实现所有这些接口:
...
public static SimpleBeanBuilder0 builder() {
return new SimpleBeanBuilder();
}
...
private static class SimpleBeanBuilder implements SimpleBeanBuilder0,
SimpleBeanBuilder1,
SimpleBeanBuilder2 {
private int index;
private String name;
public SimpleBeanBuilder1 setIndex(final int index) {
this.index = index;
return this;
}
public SimpleBeanBuilder2 setName(final String name) {
this.name = name;
return this;
}
public SimpleBean build() {
return new SimpleBean(index, name);
}
public interface SimpleBeanBuilder0 {
SimpleBeanBuilder1 setIndex(final int index);
}
public interface SimpleBeanBuilder1 {
SimpleBeanBuilder2 setName(final String name);
}
public interface SimpleBeanBuilder2 {
SimpleBean build();
}
}
这模板有点丑,换个方式。
第一步是停止实现接口,而是返回实现这些接口的匿名类:
...
public static SimpleBeanBuilder builder() {
return new SimpleBeanBuilder();
}
...
private static class SimpleBeanBuilder {
public SimpleBeanBuilder1 setIndex(int index) {
return new SimpleBeanBuilder1() {
@Override
public SimpleBeanBuilder2 setName(final String name) {
return new SimpleBeanBuilder2() {
@Override
public SimpleBean build() {
return new SimpleBean(index, name);
}
};
}
};
}
public interface SimpleBeanBuilder1 {
SimpleBeanBuilder2 setName(final String name);
}
public interface SimpleBeanBuilder2 {
SimpleBean build();
}
}
这样好多了。我们可以再次安全地SimpleBeanBuilder从该builder()方法返回,因为此类仅公开一个方法,并且不允许用户过早构建实例。但更重要的是,我们可以省略构建器中的整个设置器和可变字段样板,从而大大减少了代码量。这是可能的,因为我们在可见和可访问所有设置器参数的范围内创建了匿名类。
就代码总数而言,生成的代码与原始Builder实现相当。
但这还不是全部。由于所有匿名类实际上都是仅包含一种方法的接口的实现,因此我们可以用lambda替换匿名类:
private static class SimpleBeanBuilder {
public SimpleBeanBuilder1 setIndex(int index) {
return name -> () -> new SimpleBean(index, name);
}
public interface SimpleBeanBuilder1 {
SimpleBeanBuilder2 setName(final String name);
}
public interface SimpleBeanBuilder2 {
SimpleBean build();
}
}
注意,剩余的SimpleBeanBuilder类与其他构建器接口非常相似,因此也可以将其替换为lambda:
public static SimpleBeanBuilder builder() {
return index -> name -> () -> new SimpleBean(index, name);
}
public interface SimpleBeanBuilder {
SimpleBeanBuilder1 setIndex(int index);
}
public interface SimpleBeanBuilder1 {
SimpleBeanBuilder2 setName(final String name);
}
public interface SimpleBeanBuilder2 {
SimpleBean build();
}
最后:
- 在SimpleBeanBuilder接口内移动接口,并对接口进行一些重命名。由于这些接口很少会出现在用户代码中,因此我们可以为其使用一些标准化的命名方式,并简化代码的自动生成。
- 重命名设置器,因为不需要Java Bean命名约定,因为这里没有获取器。
- 该版本()方法是没有必要了。在最初的实现中,它表示已经完成POJO的组装,但这不再是必须的,因为一旦设置了最后一个字段,便拥有了构建POJO实例的所有必要细节。
下面是SimpleBean应用所有这些更改之后的完整代码:
public class SimpleBean {
private final int index;
private final String name;
private SimpleBean(final int index, final String name) {
this.index = index;
this.name = name;
}
public int index() {
return index;
}
public String name() {
return name;
}
public static Builder builder() {
return index -> name -> new SimpleBean(index, name);
}
public interface Builder {
Stage1 index(int index);
interface Stage1 {
SimpleBean name(final String name);
}
}
}
实际上执行少量操作的代码很少,大多数实现是一堆接口声明。添加,更改或删除字段非常简单,因为涉及的代码很少。
对于尚未习惯使用深层嵌套lambda的用户,此代码乍一看可能会比较困难,但这是经验问题。另外,无需手动编写此类代码,因为我们可以将此任务卸载到IDE(就像我们使用传统构建器一样)。
使用上述方法,我们可以用Fluent Builders代替传统的Builders,并通过Fluent API模式安全性获得Builder的简单操作。