[從 Effective Java 了解 Kotlin] — 其實你不需要建造模式(Builder Pattern)

Item 2: Consider a builder when faced with many constructor parameters

Andy Lu
10 min readJan 17, 2022
Photo by Giorgio Parravicini on Unsplash

物件導向的語言中,我們可以用類別 (class) 定義真實世界的東西,如下:

這個類別(User)是用來定義使用者的基本資料,在這個類別中,我們定義了幾個屬性:姓名(name)、年紀(age)、性別(gender)、地址(address)、電話(phone)、備註(note)

public class User {
enum Gender {
MALE,
FEMALE,
}
private final String name;
private final int age;
private final Gender gender;
private final String address;
private final String phone;
private final String note;
public User(String name, int age, Gender gender, String address, String phone, String note) {
this.name = name;
this.age = age;
this.gender = gender;
this.address = address;
this.phone = phone;
this.note = note;
}
}

最一開始的設計,我們希望每一個 User 的物件都有包含上面的每一個屬性,不過隨著時間的演進,我們最後可能會有一些改變,也許不需要帶入那麼多屬性才能建立一個物件。換句話說,我們希望其他沒有帶入的數值的屬性,能夠有一個預設值。

在 Java 中,一個類別可以有多個建構式,我們可以根據我們的需求來帶入不同的參數,進而呼叫到不同的建構式產生相對應的值。

現在我們收到了一個需求,希望在輸入使用者資料的時候,不用填那麼多資訊,所以我們定義了另一個建構式,而在這個建構式中,我們只需要傳入姓名、年紀以及性別。

public class User {
...
public User(String name, int age, Gender gender) {
this(name, age, gender, "", "", "");
}
...
}

接著,重複的事情又發生… 最後我們的類別的樣子如下:

package Ch2.Item2;public class User {
enum Gender {
MALE,
FEMALE,
}
private final String name;
private final int age;
private final Gender gender;
private final String address;
private final String phone;
private final String note;
public User(String name, int age, Gender gender) {
this(name, age, gender, "", "", "");
}
public User(String name, int age, Gender gender, String address) {
this(name, age, gender, address, "", "");
}
public User(String name, int age, Gender gender, String address, String phone) {
this(name, age, gender, address, phone, "");
}
public User(String name, int age, Gender gender, String address, String phone, String note) {
this.name = name;
this.age = age;
this.gender = gender;
this.address = address;
this.phone = phone;
this.note = note;
}
}

可以看到,在這個類別中出現了四個建構式,我們可以依據我們的需求來選擇適當的建構式,建構式沒有帶入的值,將會傳入預設值,如””。

這種寫法有什麼問題呢?

  1. 如果類別屬性有很多,那麼有可能會產生出很多個建構式,假如有三個屬性,那最多就有可能會有六種不同組合的建構式。
  2. 在每一個建構式中,沒有傳入的值都要給一個預設值,假設某一個建構式中的其中一個預設值改成別的,那麼在讀出資料的時候,有可能因為預設值不 match 而發生不可預知的錯誤。
public User(String name, int age, Gender gender) {
this(name, age, gender, "", "", "NA");
}
public User(String name, int age, Gender gender, String address) {
this(name, age, gender, address, "", "");
}

那麼,我們要怎麼滿足這個需求呢?

書中建議我們可以使用建造模式(Builder pattern),我們可以將上面的程式碼改成

package Ch2.Item2;public class UserWithBuilder {    enum Gender {
MALE,
FEMALE,
}
private final String name;
private final int age;
private final Gender gender;
private final String address;
private final String phone;
private final String note;
// Builder Pattern
public static class Builder {
private final String name;
private final int age;
private final Gender gender;
//optional
private String address = "";
private String phone = "";
private String note = "";
public Builder(String name, int age, Gender gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public Builder address(String val) {
this.address = val;
return this;
}
public Builder phone(String val) {
this.phone = val;
return this;
}
public Builder note(String val) {
this.note = val;
return this;
}
public UserWithBuilder build() {
return new UserWithBuilder(this);
}
}
private UserWithBuilder(Builder builder) { this.name = builder.name;
this.age = builder.age;
this.gender = builder.gender;
this.address = builder.address;
this.phone = builder.phone;
this.note = builder.note;
}
}

上方的類別中,在物件裡面新增了一個 public static class Builder ,這個就是建造模式的本體。在這裡面可以分成三個部分

  • Part I: 初始值

我們同樣利用建構式來帶入屬性,不同的是這邊只有帶入三個屬性,因為這三個屬性是必填的。

public Builder(String name, int age, Gender gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
  • Part II : 附加屬性

在附加屬性中,調用者可以依照需求調用相對應的函式,並且把值存入。

這邊可以發現,我們回傳的都是 Builder ,也就是說每調用一次這些函式就會把值更新在預設值上面。

public Builder address(String val) {
this.address = val;
return this;
}
public Builder phone(String val) {
this.phone = val;
return this;
}
public Builder note(String val) {
this.note = val;
return this;
}
  • Part III: 建立實體
public UserWithBuilder build() {
return new UserWithBuilder(this);
}

build() 中,我們回傳的是 UserWithBuilder 這個類別,而如果你認真看 UserWithBuilder 的建構子已經改成帶入 Builder 而不是原本的一堆屬性。

到這邊,我們就已經完成了建造模式了,利用這個模式,我們可以減少建構子的數量、定義唯一的預設值。可喜可賀。

但是,我們該如何使用 Kotlin 來實作呢?

如同標題所說,我的看法是,在 Kotlin 中,我們不需要建造模式(Builder pattern),為什麼呢?

因為 Kotlin 的建構式可以給予預設值,甚至也可以用有名稱的參數(named parameter)

上面的範例如果改用 Kotlin 來寫,會變成:

enum class Gender {
MALE, FEMALE
}
class UserK(
private val name: String,
private val age: Int,
private val gender: Gender,
private val address: String = "",
private val phone: String = "",
private val note: String = "",
)

針對必填的值,我們給予 non-null 的屬性;選填的屬性,我們可以給予預設值,或是讓它成為 nullable 的值。(上面的範例我採用的是給予預設值)

這樣就定義完成了一個類別,當然我們也可以使用 data class 來取代一般的 class。

調用建構式也很容易:

我們可以直接使用 屬性名稱=值 的寫法來傳入數值,這樣一來就算有相同型別的屬性,我們也不會被混淆。

fun main() {
UserK(name = "Andy", age = 35, gender = Gender.MALE)
}

另外,如果必填的值少了一個,也會立刻通知我們。

結論

由於 Java 語言的特性,只允許多個建構式,而不允許在建構式中給予初始值,所以如果有很多屬性為了要滿足所有可能性,那就有可能會產生出超多建構式。對於這種問題,我們可以使用建造模式來用函式的方式寫入屬性值,這麼一來,非必填的屬性就不需要寫在建構式中,而是使用函式的呼叫方式。而且呼叫的順序也沒有限制,這樣可以讓類別的建立更為方便。

不過,在 Kotlin 中因為支援屬性預設值的緣故,我們可以把非必填的數值給予初始值,如此一來我們就不用在建立物件的時候帶入其值。

在建構式中另外可以用屬性名稱加上等號,那麼建構式就會變得更加清晰而不需要按照順序來帶入。

所以我認為在 Kotlin 中,是不需要使用建構模式的。

謝謝大家。

你的鼓勵是我的成長的動力

--

--

Andy Lu
Andy Lu

Written by Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.

No responses yet