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。
为避免此类问题,**我们需要在IntelliJ 或Eclipse **中启用注解处理。在这样做的同时,我们将使用 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.Builder与Address类型一起作为输入的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. 与其他构建器库的比较
我们在本文中讨论的构建器实现与Lombok 、Immutables 或任何其他注解处理器 的实现非常相似。但是,我们已经讨论了一些显着特征 :
-
- 映射器方法
- 嵌套的可构建类型
- 部分对象
- 映射器方法