Builder模式是在Java中最流行的模式之一。它很简单,有助于保持对象不可变,并且可以使用Project Lombok的@Builder或Immutables等工具生成,仅举几例。
模式的流畅变体示例:
public class User {
private final String firstName;
private final String lastName;
User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; }
public static Builder builder() { return new Builder(); }
public static class Builder {
String firstName; String lastName;
Builder firstName(String value) { this.firstName = value; return this; }
Builder lastName(String value) { this.lastName = value; return this; }
public User build() { return new User(firstName, lastName); } } }
|
调用方式:
User.Builder builder = User.builder().firstName("Sergey").lastName("Egorov");
if (newRules) { builder.firstName("Sergei"); }
User user = builder.build();
|
解释:
- User class是不可变的,一旦我们实例化它就无法更改。
- 它的构造函数具有包私有可见性,必须使用构建器来实例化实例User。
- Builder的字段不是不可变的,可以在构建实例之前多次更改User。
- builder流利并且返回this(类型Builder)并且可以链接。
有什么问题?继承问题
想象一下,我们想扩展User类:(banq注:其实如果User类是DDD值对象,实际是 final class,不能再被继承了)。
public class RussianUser extends User { final String patronymic;
RussianUser(String firstName, String lastName, String patronymic) { super(firstName, lastName); this.patronymic = patronymic; }
public static RussianUser.Builder builder() { return new RussianUser.Builder(); }
public static class Builder extends User.Builder {
String patronymic;
public Builder patronymic(String patronymic) { this.patronymic = patronymic; return this; }
public RussianUser build() { return new RussianUser(firstName, lastName, patronymic); } } }
|
调用代码时会出错:
RussianUser me = RussianUser.builder() .firstName("Sergei") // returns User.Builder :( .patronymic("Valeryevich") // // Cannot resolve method!出错 .lastName("Egorov") .build();
|
这里的问题是因为firstName有以下定义:
User.Builder firstName(String value) { this.value = value; return this; }
|
Java的编译器无法检测到this的意思是RussianUser.Builder而不是User.Builder!
我们甚至无法改变顺序:
RussianUser me = RussianUser.builder() .patronymic("Valeryevich") .firstName("Sergei") .lastName("Egorov") .build() // compilation error! User is not assignable to RussianUser ;
|
可能的解决方案:Self typing
解决它的一种方法是添加一个泛型参数User.Builder,指示要返回的类型:
public static class Builder<SELF extends Builder<SELF>> {
SELF firstName(String value) { this.firstName = value; return (SELF) this; }
|
并将其设置为RussianUser.Builder:
public static class Builder extends User.Builder<RussianUser.Builder> {
|
它现在有效:
RussianUser.builder() .firstName("Sergei") // returns RussianUser.Builder :) .patronymic("Valeryevich") // RussianUser.Builder .lastName("Egorov") // RussianUser.Builder .build(); // RussianUser
|
它还适用于多级继承:
class A<SELF extends A<SELF>> {
SELF self() { return (SELF) this; } }
class B<SELF extends B<SELF>> extends A<SELF> {}
class C extends B<C> {}
|
那么,问题解决了吗?好吧,不是真的... 基本类型不能轻易实例化!
因为它使用递归泛型定义,所以我们有一个递归问题!
new A<A<A<A<A<A<A<...>>>>>>>()
但是,它可以解决(除非你使用Kotlin):
A a = new A<>();
在这里,我们依赖于Java的原始类型和钻石运算符<>。
但是,正如所提到的,它不适用于其他语言,如Kotlin或Scala,并且一般来说是这是一种黑客方式。
理想的解决方案:使用Java的Self typing
在继续阅读之前,我应该警告你:这个解决方案不存在,至少现在还没有。拥有它会很好,但目前我不知道任何JEP。PS谁知道如何提交JEP?;)
Self typing作为语言功能存在于Swift等语言中。
想象一下以下虚构的Java伪代码示例:
class A {
@Self void withSomething() { System.out.println("something"); } }
class B extends A { @Self void withSomethingElse() { System.out.println("something else"); } }
|
调用:
new B() .withSomething() // replaced with the receiver instead of void .withSomethingElse();
|
如您所见,问题可以在编译器级别解决。事实上,有像Manifold的@Self这样的 javac编译器插件。
真正的解决方案:想一想
但是,如果不是试图解决返回类型问题,我们...删除类型?
public class User {
// ...
public static class Builder {
String firstName; String lastName;
void firstName(String value) { this.firstName = value; }
void lastName(String value) { this.lastName = value; }
public User build() { return new User(firstName, lastName); } } } public class RussianUser extends User {
// ...
public static class Builder extends User.Builder {
String patronymic;
public void patronymic(String patronymic) { this.patronymic = patronymic; }
public RussianUser build() { return new RussianUser(firstName, lastName, patronymic); } } }
|
调用方式:
RussianUser.Builder b = RussianUser.builder(); b.firstName("Sergei"); b.patronymic("Valeryevich"); b.lastName("Egorov"); RussianUser user = b.build(); // RussianUser
|
你可能会说,“这不是方便而且冗长,至少在Java中”。我同意,但......这是Builder的问题吗?
还记得我说过这个Builder是可变的吗?那么,为什么不利用它呢!
让我们将以下内容添加到我们的基础构建器中:
public class User {
// ...
public static class Builder { public Builder() { this.configure(); }
protected void configure() {}
|
并使用我们的构建器作为匿名对象:
RussianUser user = new RussianUser.Builder() { @Override protected void configure() { firstName("Sergei"); // from User.Builder patronymic("Valeryevich"); // From RussianUser.Builder lastName("Egorov"); // from User.Builder } }.build();
|
继承不再是一个问题,但它仍然有点冗长。
这里是Java的另一个“特性”派上用场: Double brace initialization/双大括号初始化。
这里我们使用初始化块来设置字段。Swing / Vaadin人可能认识到这种模式;)
有些人不喜欢它(随意评论为什么,顺便说一句)。我不会在应用程序的性能关键部分使用它,但如果是,比方说,测试,那么这种方法似乎标记了所有检查:
- 可以与从Mammoths Age开始的任何Java版本一起使用。
- 对其他JVM语言友好。
- 简洁。
- 语言的本机特性,而不是黑客。
结论
我们已经看到,虽然Java不提供自键型语法,但我们可以通过使用Java的另一个功能来解决问题,而不会破坏替代JVM语言的体验。
虽然一些开发人员似乎认为双大括号初始化是一种反模式,但它实际上似乎对某些用例有其价值。毕竟,这只是匿名类中构造函数定义的糖。