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