生成器模式在Java应用程序中非常流行。但它经常被大家误解和错误地使用,从而导致运行时错误。
让我们记住使用Builder的目的:仅在某些对象中设置必要的字段,并将其余字段设置为默认值。例如,如果我们正在准备配置对象,那么仅更改必要的参数并将其他参数设置为默认值会很方便。
许多开发人员只选择了Builder模式的一部分:可以单独设置字段。第二部分:其余字段存在合理的默认值-通常会被忽略。
结果,很容易获得不完整的(部分初始化的)POJO。为了缓解此问题,我们对build()方法进行了检查,最后可能自己都没发现啥问题但就是出错。此时此刻,主要的损害已经造成:检查转移到了运行时间上。为确保一切正常,我们需要添加专用测试以覆盖创建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(); }
最后:
下面是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的简单操作。