使用FreeBuilder自动生成构建者模式

2023/05/26

1. 概述

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

2. 建造者设计模式

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

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

3. Java中的Builder实现

在我们使用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("tuyucheng")
    .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现在是一个接口而不是一个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("tuyucheng")
    .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("tuyucheng")
    .setAge(10)
    .setDepartment("Builder Pattern")
    .build();

5.3 映射器方法

结合getter和setter,FreeBuilder还在构建器类中添加了映射器方法。这些映射器方法接收UnaryOperator作为输入参数,从而允许开发人员计算复杂的字段值。

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

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

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

long salaryInEuros = INPUT_SALARY_EUROS;
Employee.Builder builder = new Employee.Builder();

Employee employee = builder
    .setName("tuyucheng")
    .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 约束检查

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

这样的约束要求我们对输入值进行验证,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 {
    }
}

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

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

值得注意的是,我们只是传递了强制字段的值而没有提供Optional字段的值,由于我们没有为dateOfJoining字段设置值,因此它将是Optional.empty(),这是Optional字段的默认值。

7.2 使用@Nullable字段

尽管在Java中建议使用Optional来处理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. Collections和Maps

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添加了方便的方法来将输入元素添加到构建器类中的集合中:

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

构建器类中还有一个getAccessTokens()方法,它返回一个不可修改的集合。同样,对于Map:

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

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

9. 嵌套构建器

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

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

@FreeBuilder
public interface Address {
 
    String getCity();

    class Builder extends Address_Builder {
    }
}

现在,FreeBuilder生成将Address.Builder作为输入以及Address类型的setter方法:

Address.Builder addressBuilder = new Address.Builder();
addressBuilder.setCity(CITY_NAME);

Employee employee = builder.setName("tuyucheng")
    .setAddress(addressBuilder)
    .build();

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

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

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

10. 构建部分对象

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

虽然这对于生产环境来说是理想的,但它通常会使独立于约束的单元测试变得复杂。

为了放宽这些限制,FreeBuilder允许我们构建部分对象:

Employee employee = builder.setName("tuyucheng")
    .setAge(10)
    .setEmail("abc@xyz.com")
    .buildPartial();

assertNotNull(employee.getEmail());

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

11. 自定义toString()方法

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

@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或任何其他注解处理器的构建器实现非常相似。但是,我们已经讨论了一些显著特征:

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

13. 总结

在本文中,我们使用FreeBuilder库在Java中生成构建器类,在注解的帮助下我们实现了构建器类的各种自定义,从而减少了其实现所需的样板代码。我们还了解了FreeBuilder与其他一些库的不同之处,并在本文中简要讨论了其中的一些特性。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章