Contents

Java 中构建器类

1. 概述

在本教程中,我们将使用FreeBuilder 库 在 Java 中生成构建器类。

2. 建造者设计模式

Builder 是面向对象语言中使用最广泛的创建设计模式 之一。它抽象了复杂领域对象的实例化,并提供了一个流畅的 API来创建实例。因此,它有助于保持简洁的域层。

尽管它很有用,但构建器通常很难实现,尤其是在 Java 中。更简单的值对象也需要大量样板代码。

3. Java 中的构建器实现

在我们继续使用 FreeBuilder 之前,让我们为Employee类实现一个样板构建器:

public class Employee {
    private final String name;
    private final int age;
    private final String department;
    private Employee(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }
}

还有一个内部 Builder类:

public static class Builder {
    private String name;
    private int age;
    private String department;
    public Builder setName(String name) {
        this.name = name;
        return this;
    }
    public Builder setAge(int age) {
        this.age = age;
        return this;
    }
    public Builder setDepartment(String department) {
        this.department = department;
        return this;
    }
    public Employee build() {
        return new Employee(name, age, department);
    }
}

因此,我们现在可以使用构建器来实例化Employee对象:

Employee.Builder emplBuilder = new Employee.Builder();
Employee employee = emplBuilder
  .setName("blogdemo")
  .setAge(12)
  .setDepartment("Builder Pattern")
  .build();

如上所示,实现构建器类需要大量样板代码。

在后面的部分中,我们将看到 FreeBuilder 如何立即简化此实现。

4. Maven依赖

要添加 FreeBuilder 库,我们将在pom.xml中添加FreeBuilder Maven 依赖 项:

<dependency>
    <groupId>org.inferred</groupId>
    <artifactId>freebuilder</artifactId>
    <version>2.4.1</version>
</dependency>

5. FreeBuilder注解

5.1. 生成生成器

FreeBuilder 是一个开源库,可帮助开发人员在实现构建器类时避免使用样板代码。它利用 Java 中的注释处理来生成构建器模式的具体实现。

我们将使用***@FreeBuilder注解前面部分中的Employee*类,**并查看它如何自动生成构建器类:

@FreeBuilder
public interface Employee {
 
    String name();
    int age();
    String department();
    
    class Builder extends Employee_Builder {
    }
}

重要的是要指出 Employee现在是一个interface**而不是 POJO 类。此外,它包含 Employee对象的所有属性作为方法。

在我们继续使用这个构建器之前,我们必须配置我们的 IDE 以避免任何编译问题。由于FreeBuilder在编译期间自动生成 Employee_Builder类,IDE通常会在第 8 行抱怨ClassNotFoundException

为避免此类问题,**我们需要在IntelliJEclipse **中启用注解处理。在这样做的同时,我们将使用 FreeBuilder 的注解处理器 org.inferred.freebuilder.processor.Processor。此外,用于生成这些源文件的目录应标记为Generated Sources Root

或者,我们也可以执行mvn install来构建项目并生成所需的构建器类。

最后,我们已经编译了我们的项目,现在可以使用Employee.Builder类:

Employee.Builder builder = new Employee.Builder();
 
Employee employee = builder.name("blogdemo")
  .age(10)
  .department("Builder Pattern")
  .build();

总而言之,这与我们之前看到的构建器类之间有两个主要区别。首先,我们必须为Employee类的所有属性设置值。否则,它会抛出IllegalStateException。**

我们将在后面的部分看到 FreeBuilder 如何处理可选属性。

其次,Employee.Builder的方法名称不遵循 JavaBean 命名约定。我们将在下一节中看到这一点。

5.2. JavaBean 命名约定

为了强制 FreeBuilder 遵循 JavaBean 命名约定,我们必须重命名 Employee中的方法并在方法前加上get

@FreeBuilder
public interface Employee {
 
    String getName();
    int getAge();
    String getDepartment();
    class Builder extends Employee_Builder {
    }
}

这将生成遵循 JavaBean 命名约定的 getter 和 setter:

Employee employee = builder
  .setName("blogdemo")
  .setAge(10)
  .setDepartment("Builder Pattern")
  .build();

5.3. 映射器方法

再加上 getter 和 setter,FreeBuilder 还在 builder 类中添加了 mapper 方法。这些映射器方法**接受UnaryOperator 作为输入,**从而允许开发人员计算复杂的字段值。

假设我们的Employee类也有一个薪水字段:

@FreeBuilder
public interface Employee {
    Optional<Double> getSalaryInUSD();
}

现在假设我们需要转换作为输入提供的工资的货币:

long salaryInEuros = INPUT_SALARY_EUROS;
Employee.Builder builder = new Employee.Builder();
Employee employee = builder
  .setName("blogdemo")
  .setAge(10)
  .mapSalaryInUSD(sal -> salaryInEuros * EUROS_TO_USD_RATIO)
  .build();

FreeBuilder 为所有字段提供了这样的映射器方法。

6. 默认值和约束检查

6.1. 设置默认值

到目前为止,我们讨论的Employee.Builder实现期望客户端传递所有字段的值。事实上,如果缺少字段,它会以IllegalStateException失败初始化过程。

为了避免此类失败,我们可以为字段设置默认值或将它们设为可选

我们可以在 Employee.Builder构造函数中设置默认值:

@FreeBuilder
public interface Employee {
    // getter methods
    class Builder extends Employee_Builder {
        public Builder() {
            setDepartment("Builder Pattern");
        }
    }
}

所以我们只需在构造函数中设置默认Department。此值将应用于所有Employee对象。

6.2. 约束检查

通常,我们对字段值有一定的限制。例如,有效的电子邮件必须包含“@”,或者Employee的年龄必须在一个范围内。

这种约束要求我们对输入值进行验证。FreeBuilder允许我们仅通过覆盖 setter方法来添加这些验证:

@FreeBuilder
public interface Employee {
    // getter methods
    class Builder extends Employee_Builder {
        @Override
        public Builder setEmail(String email) {
            if (checkValidEmail(email))
                return super.setEmail(email);
            else
                throw new IllegalArgumentException("Invalid email");
        }
        private boolean checkValidEmail(String email) {
            return email.contains("@");
        }
    }
}

7. 可选值

7.1. 使用Optional字段

某些对象包含可选字段,其值可以为空或 null。FreeBuilder 允许我们使用Java Optional 类型定义这些字段:

@FreeBuilder
public interface Employee {
    String getName();
    int getAge();
    // other getters
    
    Optional<Boolean> getPermanent();
    Optional<String> getDateOfJoining();
    class Builder extends Employee_Builder {
    }
}

现在我们可以跳过为Optional字段提供任何值:

Employee employee = builder.setName("blogdemo")
  .setAge(10)
  .setPermanent(true)
  .build();

值得注意的是,我们只是传递了Permanent字段的值而不是Optional。由于我们没有设置dateOfJoining字段的值 ,它将是 Optional.empty(),这是 Optional字段的默认值。

7.2. 使用*@Nullable*字段

尽管推荐使用Optional来处理Java 中的null,但 FreeBuilder 允许*我们使用*@Nullable 来实现向后兼容性

@FreeBuilder
public interface Employee {
    String getName();
    int getAge();
    
    // other getter methods
    Optional<Boolean> getPermanent();
    Optional<String> getDateOfJoining();
    @Nullable String getCurrentProject();
    class Builder extends Employee_Builder {
    }
}

在某些情况下不建议使用*Optional * ,这也是为什么*@Nullable*更适合构建器类的另一个原因。

8. 集合和Map

FreeBuilder对集合和Map有特殊的支持:

@FreeBuilder
public interface Employee {
    String getName();
    int getAge();
    
    // other getter methods
    List<Long> getAccessTokens();
    Map<String, Long> getAssetsSerialIdMapping();
    class Builder extends Employee_Builder {
    }
}

FreeBuilder在 builder 类中添加了方便的方法来将输入元素添加到 Collection 中

Employee employee = builder.setName("blogdemo")
  .setAge(10)
  .addAccessTokens(1221819L)
  .addAccessTokens(1223441L, 134567L)
  .build();

builder类中还有一个getAccessTokens()方法,它返回一个不可修改的列表。同样,对于map

Employee employee = builder.setName("blogdemo")
  .setAge(10)
  .addAccessTokens(1221819L)
  .addAccessTokens(1223441L, 134567L)
  .putAssetsSerialIdMapping("Laptop", 12345L)
  .build();

Map的 getter方法 还向客户端代码返回一个不可修改的映射。

9. 嵌套构建器

对于现实世界的应用程序,我们可能必须为我们的领域实体嵌套很多值对象。而且由于嵌套对象本身可能需要构建器实现,因此 FreeBuilder 允许嵌套可构建类型。

例如,假设我们 在Employee类中有一个嵌套的复杂类型Address

@FreeBuilder
public interface Address {
 
    String getCity();
    class Builder extends Address_Builder {
    }
}

现在,FreeBuilder 生成 将Address.BuilderAddress类型一起作为输入的setter方法 :

Address.Builder addressBuilder = new Address.Builder();
addressBuilder.setCity(CITY_NAME);
Employee employee = builder.setName("blogdemo")
  .setAddress(addressBuilder)
  .build();

值得注意的是,FreeBuilder 还添加了一个方法来自定义** Employee中现有的Address对象:**

Employee employee = builder.setName("blogdemo")
  .setAddress(addressBuilder)
  .mutateAddress(a -> a.setPinCode(112200))
  .build();

除了 FreeBuilder类型,FreeBuilder 还允许嵌套其他构建器,例如protos

10. 构建部分对象

正如我们之前讨论过的,FreeBuilder 会针对任何违反约束的情况抛出IllegalStateException ——例如,必填字段的缺失值。

尽管这对于生产环境来说是理想的,但它使独立于一般约束的单元测试变得复杂。

为了放松这些约束,FreeBuilder 允许我们构建部分对象:

Employee employee = builder.setName("blogdemo")
  .setAge(10)
  .setEmail("[[email protected]](/cdn_cgi/l/email_protection)")
  .buildPartial();
assertNotNull(employee.getEmail());

因此,即使我们没有为Employee设置所有必填字段,我们仍然可以验证email字段是否具有有效值。

11. 自定义*toString()*方法

对于值对象,**我们经常需要添加一个自定义的toString()实现。**FreeBuilder 通过abstract类允许这样做:

@FreeBuilder
public abstract class Employee {
    abstract String getName();
    abstract int getAge();
    @Override
    public String toString() {
        return getName() + " (" + getAge() + " years old)";
    }
    public static class Builder extends Employee_Builder{
    }
}

我们将 Employee声明为一个抽象类而不是一个接口,并提供了一个自定义的*toString()*实现。

12. 与其他构建器库的比较

我们在本文中讨论的构建器实现与LombokImmutables 或任何其他注解处理器 的实现非常相似。但是,我们已经讨论了一些显着特征 :

    • 映射器方法
      • 嵌套的可构建类型
      • 部分对象