Builder模式與Java語法
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 class是不可變的,一旦我們例項化它就無法更改。
- 它的建構函式具有包私有可見性,必須使用構建器來例項化例項User。
- Builder的欄位不是不可變的,可以在構建例項之前多次更改User。
- builder流利並且返回this(型別Builder)並且可以連結。
有什麼問題?
繼承問題
想象一下,我們想擴充套件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人可能認識到這種模式;)
有些人不喜歡它(隨意評論為什麼,順便說一句)。我不會在應用程式的效能關鍵部分使用它,但如果是,比方說,測試,那麼這種方法似乎標記了所有檢查:
- 可以與從Mammoths Age開始的任何Java版本一起使用。
- 對其他JVM語言友好。
- 簡潔。
- 語言的本機特性,而不是黑客。
結論
我們已經看到,雖然Java不提供自鍵型語法,但我們可以通過使用Java的另一個功能來解決問題,而不會破壞替代JVM語言的體驗。
雖然一些開發人員似乎認為雙大括號初始化是一種反模式,但它實際上似乎對某些用例有其價值。畢竟,這只是匿名類中建構函式定義的糖。