Builder模式與Java語法

banq發表於2019-02-06

Builder模式是在Java中最流行的模式之一。它很簡單,有助於保持物件不可變,並且可以使用Project Lombok的@BuilderImmutables等工具生成,僅舉幾例。
模式的流暢變體示例:

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();


解釋:
  1. User class是不可變的,一旦我們例項化它就無法更改。
  2. 它的建構函式具有包私有可見性,必須使用構建器來例項化例項User。
  3. Builder的欄位不是不可變的,可以在構建例項之前多次更改User。
  4. 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人可能認識到這種模式;)
有些人不喜歡它(隨意評論為什麼,順便說一句)。我不會在應用程式的效能關鍵部分使用它,但如果是,比方說,測試,那麼這種方法似乎標記了所有檢查:
  1. 可以與從Mammoths Age開始的任何Java版本一起使用。
  2. 對其他JVM語言友好。
  3. 簡潔。
  4. 語言的本機特性,而不是駭客。


結論
我們已經看到,雖然Java不提供自鍵型語法,但我們可以透過使用Java的另一個功能來解決問題,而不會破壞替代JVM語言的體驗。
雖然一些開發人員似乎認為雙大括號初始化是一種反模式,但它實際上似乎對某些用例有其價值。畢竟,這只是匿名類中建構函式定義的糖。

相關文章