java:其实你并不会用Builder

createh53周前 (12-09)技术教程20

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

相关文章

Java 编程技巧之单元测试用例编写流程

温馨提示:本文较长,同学们可收藏后再看 :)前言清代杰出思想家章学诚有一句名言:“学必求其心得,业必贵其专精。”意思是:学习上一定要追求心得体会,事业上一定要贵以专注精深。做技术就是这样,一件事如果做...

Java主要的5个标准注解如何使用?

Java主要提供了5个标准注解,分别是:OverrideDeprecatedSuppressWarningsSafeVarargsFunctionalInterface本文将从“如何使用这5个标准注解...

Java教程:gitlab-使用入门

1 导读本教程主要讲解了GitLab在项目的环境搭建和基本的使用,可以帮助大家在企业中能够自主搭建GitLab服务,并且可以GitLab中的组、权限、项目自主操作GitLab简介GitLab环境搭建G...

如何自学JAVA

一:Java基础知识,俗话说的好“千里之行,始于足下”,学习也是一样的从小的基础的知识点开始慢慢积累,掌握Java语言的基础知识,如面向对象、数据结构与算法、异常处理、IO框架、多线程、网络编程、设计...

「Java基础」带你深入理解 Spring:入门+使用+原理

Spring 事务是复杂一致性业务必备的知识点,掌握好 Spring 事务可以让我们写出更好地代码。这篇文章我们将介绍 Spring 事务的诞生背景,从而让我们可以更清晰地了解 Spring 事务存在...