設計模式之Builder模式

轉載自:http://www.jianshu.com/p/e2a2fe3555b9

Builder模式是怎麼來的

不知道是哪位賢人曾經說過,存在即爲合理。Builder模式在衆多的框架以及android原生代碼中存在(比如AlertDialog),就一定有其價值,用來解決某些需求。

考慮這樣一個場景,假如有一個類(User),裏面有很多屬性,並且你希望這些類的屬性都是不可變的(final),就像下面的代碼:

public class User {

    private final String firstName;     // 必傳參數
    private final String lastName;      // 必傳參數
    private final int age;              // 可選參數
    private final String phone;         // 可選參數
    private final String address;       // 可選參數
}

在這個類中,有些參數是必要的,而有些參數是非必要的。就好比在註冊用戶時,用戶的姓和名是必填的,而年齡、手機號和家庭地址等是非必需的。那麼問題就來了,如何創建這個類的對象呢?

一種可行的方案就是實用構造方法。第一個構造方法只包含兩個必需的參數,第二個構造方法中,增加一個可選參數,第三個構造方法中再增加一個可選參數,依次類推,直到構造方法中包含了所有的參數。

    public User(String firstName, String lastName) {
        this(firstName, lastName, 0);
    }

    public User(String firstName, String lastName, int age) {
        this(firstName, lastName, age, "");
    }

    public User(String firstName, String lastName, int age, String phone) {
        this(firstName, lastName, age, phone, "");
    }

    public User(String firstName, String lastName, int age, String phone, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.phone = phone;
        this.address = address;
    }

這樣做的好處只有一個:可以成功運行。但是弊端很明顯:

  • 參數較少的時候問題還不大,一旦參數多了,代碼可讀性就很差,並且難以維護。
  • 對調用者來說也很麻煩。如果我只想多傳一個address參數,還必需給agephone設置默認值。而且調用者還會有這樣的困惑:我怎麼知道第四個String類型的參數該傳address還是phone

第二種解決辦法就出現了,我們同樣可以根據JavaBean的習慣,設置一個空參數的構造方法,然後爲每一個屬性設置settersgetters方法。就像下面一樣:

public class User {

    private String firstName;     // 必傳參數
    private String lastName;      // 必傳參數
    private int age;              // 可選參數
    private String phone;         // 可選參數
    private String address;       // 可選參數

    public User() {
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }

    public String getPhone() {
        return phone;
    }

    public String getAddress() {
        return address;
    }
}

這種方法看起來可讀性不錯,而且易於維護。作爲調用者,創建一個空的對象,然後只需傳入我感興趣的參數。那麼缺點呢?也有兩點:

  • 對象會產生不一致的狀態。當你想要傳入5個參數的時候,你必需將所有的setXX方法調用完成之後才行。然而一部分的調用者看到了這個對象後,以爲這個對象已經創建完畢,就直接食用了,其實User對象並沒有創建完成。
  • User類是可變的了,不可變類所有好處都不復存在。

終於輪到主角上場的時候了,利用Builder模式,我們可以解決上面的問題,代碼如下:

public class User {

    private final String firstName;     // 必傳參數
    private final String lastName;      // 必傳參數
    private final int age;              // 可選參數
    private final String phone;         // 可選參數
    private final String address;       // 可選參數

    private User(UserBuilder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phone = builder.phone;
        this.address = builder.address;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }

    public String getPhone() {
        return phone;
    }

    public String getAddress() {
        return address;
    }

    public static class UserBuilder {
        private final String firstName;
        private final String lastName;
        private int age;
        private String phone;
        private String address;

        public UserBuilder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public UserBuilder age(int age) {
            this.age = age;
            return this;
        }

        public UserBuilder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public UserBuilder address(String address) {
            this.address = address;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

有幾個重要的地方需要強調一下:

  • User類的構造方法是私有的。也就是說調用者不能直接創建User對象。
  • User類的屬性都是不可變的。所有的屬性都添加了final修飾符,並且在構造方法中設置了值。並且,對外只提供getters方法。
  • Builder模式使用了鏈式調用。可讀性更佳。
  • Builder的內部類構造方法中只接收必傳的參數,並且該必傳的參數適用了final修飾符。

相比於前面兩種方法,Builder模式擁有其所有的優點,而沒有上述方法中的缺點。客戶端的代碼更容易寫,並且更重要的是,可讀性非常好。唯一可能存在的問題就是會產生多餘的Builder對象,消耗內存。然而大多數情況下我們的Builder內部類使用的是靜態修飾的(static),所以這個問題也沒多大關係。

現在,讓我們看看如何創建一個User對象呢?

new User.UserBuilder("王", "小二")
                .age(20)
                .phone("123456789")
                .address("亞特蘭蒂斯大陸")
                .build();

相當整潔,不是嗎?你甚至可以用一行代碼完成對象的創建。

關於Builder的一點說明

線程安全問題

由於Builder是非線程安全的,所以如果要在Builder內部類中檢查一個參數的合法性,必需要在對象創建完成之後再檢查。

public User build() {
  User user = new user(this);
  if (user.getAge() > 120) {
    throw new IllegalStateException(“Age out of range”); // 線程安全
  }
  return user;
}

上面的寫法是正確的,而下面的代碼是非線程安全的:

public User build() {
  if (age > 120) {
    throw new IllegalStateException(“Age out of range”); // 非線程安全
  }
  return new User(this);
}

經典的Builder模式

上面介紹的Builder模式當然不是“原生態”的啦,經典的Builder模式的類圖如下:


builder

其中:

  • Product 產品抽象類。
  • Builder 抽象的Builder類。
  • ConcretBuilder 具體的Builder類。
  • Director 同一組裝過程。

當然,之前實例中的Builder模式,是省略掉了Director的,這樣結構更加簡單。所以在很多框架源碼中,涉及到Builder模式時,大多都不是經典GOF的Builder模式,而是省略後的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章