揭秘Java:你可能误解了Builder的使用

发表时间: 2020-07-27 15:37

生成器模式在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的简单操作。