Java中记录生成器RecordBuilder实用指南

在Java 16中引入,提供了一种简洁的建模方法不可变数据.它们自动生成构造函数、访问函数、equals(),hashCode()得双曲正弦值.字符串()方法,减少样板和提高可读性。

尽管有这些好处,记录也有明显的局限性。例如,所有字段必须在记录头中声明,不允许使用setter方法,并且由于其隐式final性质,可扩展性受到限制。当我们需要灵活的对象创建时,这些约束使记录不太理想,特别是在涉及可选参数或修改现有实例的场景中。

为了解决这些限制,RecordBuilder库提供了一个简单而有效的解决方案。它通过构建器模式增强记录,弥合了优雅的不变性和灵活构建的实用性之间的差距。

RecordBuilder库使使用Java记录变得更容易和更实用。它生成流畅的、不可变的友好构建器,消除了对手动构造函数或更新方法的需要。

它的真实的优势在于平衡:灵活的更新而不失去不变性,富有表现力的代码而不带样板。无论我们是构建DTO、API响应还是配置对象,它都自然地适合我们的工作流。几乎不需要任何设置,RecordBuilder就成为了一个干净、可伸缩的Java开发的强大工具。

要开始使用RecordBuilder,我们首先需要将注释处理器添加到构建设置中。对于Maven用户来说,这看起来像:

<dependency>
    <groupId>io.soabase.record-builder</groupId>
    <artifactId>record-builder-core</artifactId>
    <version>47</version>
</dependency>

设置完成后,我们将使用@RecordBuilder注释记录,并可选地实现它生成的With接口,以启用内联构建器和fluent withX()方法。

为什么使用RecordBuilder?
乍看之下,纯形式的记录是强大的-它们以最少的语法为我们提供了简洁,不可变的数据结构:

public record Person(String name, int age) {}

然而,它们的全参数构造函数和缺乏setter使它们在我们需要灵活性时变得僵硬。在我们使用可选或可变值构建数据的领域中,这可能很快就会受到限制。我们可以编写自己的构建器来解决这个问题,但这样做会重新引入记录本应消除的样板文件。

RecordBuilder优雅地弥合了这一差距。通过单个注释,我们获得了一种流畅、安全和可读的方式来构建和修改记录实例。它提供了对分段构建器、withX()方法、自定义钩子等的支持--所有这些都尊重不变性。例如,让我们假设我们像这样注释我们的记录:

@RecordBuilder
public record Person(String name, int age) implements PersonBuilder.With {}

从这里,RecordBuilder生成了一整套构建器方法,包括X()访问器,甚至静态工厂帮助器,所有这些都遵循了不变性的最佳实践。

使用RecordBuilder生成器
让我们通过构建器改进工作流的一些实际方法,使用RecordBuilder演示课

首先,我们以标准的方式构造初始记录:

Person p1 = new Person("foo", 123);
assertEquals(
"foo", p1.name());
assertEquals(123, p1.age());

接下来,我们通过生成的withName()或withAge()方法更新各个字段:

Person p2 = p1.withName("bar");
assertEquals(
"bar", p2.name());
assertEquals(123, p2.age());
Person p3 = p2.withAge(456);
assertEquals(
"bar", p3.name());
assertEquals(456, p3.age());

在这里,我们保留了不变性,并且在每次转换时只修改预期的字段。尽管如此,RecordBuilder提供了更多。我们可以使用流畅的构建器语法在前一个实例的基础上构建一个全新的实例:

Person p4 = p3.with()
  .age(101)
  .name("baz")
  .build();
assertEquals(
"baz", p4.name());
assertEquals(101, p4.age());

当需要更改多个字段时,此样式可提高清晰度。它避免了构造函数链接,并使每个转换的意图显式。正如我们接下来将看到的,构建器还支持内联的基于消费者的更新,以实现更具表达力的修改逻辑。

高级功能
RecordBuilder的亮点之一是支持内联的、基于消费者的修改。下面是我们如何使用lambda修改记录:

Person p5 = p4.with(p -> p.age(200).name("whatever"));
assertEquals(
"whatever", p5.name());
assertEquals(200, p5.age());

此外,我们可以在构建器上下文中应用条件逻辑:

Person p6 = p5.with(p -> {
    if (p.age() > 13) {
        p.name("Teen " + p.name());
    } else {
        p.name(
"whatever");
    }
});
assertEquals(
"Teen whatever", p6.name());
assertEquals(200, p6.age());

为了获得更多的控制,我们还可以使用静态构建器工厂,当在记录实例之外操作时特别有用:

Person p7 = PersonBuilder.from(p6)
  .with(p -> p.age(300).name("Manual Copy"));
assertEquals(
"Manual Copy", p7.name());
assertEquals(300, p7.age());

或者,我们可以直接将更新应用于单个字段:

Person p8 = PersonBuilder.from(p6)
  .withName("boop");
assertEquals(
"boop", p8.name());
assertEquals(200, p8.age());

当使用分离的构建器、外部数据转换或测试实用程序时,此静态表单特别有用。因此,它提供了构造逻辑和业务逻辑之间的清晰分离,使构建器模式成为跨服务层的通用工具。

定制和选项
有了RecordBuilder,我们超越了基本的构建器生成,提供了定制功能,让我们适应我们的特定领域的需要构建器。通过这种方式,我们支持分段构建器,以便在编译时强制执行必需字段,确保在构建之前始终设置基本值。这有助于我们防止细微的运行时错误,并提高对象创建逻辑的安全性。

我们还可以将RecordBuilder与测试框架或依赖注入工具集成,使其成为设置fixture或注入部分构造对象的绝佳选择。将构建器逻辑与应用程序体系结构保持一致的能力-而不会失去不变性的好处-使RecordBuilder既灵活又可用于生产。

对比:RecordBuilder与手动生成器
我们完全可以为记录编写自己的构建器,特别是当它只有几个字段时。然而,随着记录的增长和发展,手动构建器很快就成为一个令人头痛的维护问题。对记录的每次更改都需要在构建器中进行更新:新字段、构造器逻辑、withX()方法和build()逻辑都需要手动调整。

RecordBuilder通过注释处理消除了这种负担。它在编译时生成构建器,并使其与记录的结构完全同步。对记录标题的任何修改-添加、删除或重新排序字段-都将自动处理。这消除了常见的错误源,并提高了长期的可维护性。

此外,RecordBuilder引入了一些有用的模式,如基于消费者的更新和静态克隆方法,这些模式需要大量的手动实现。就开发人员的效率、正确性和一致性而言,RecordBuilder提供了比手工构建器更多的价值。