若要開發一個HTTP程式庫,在建立請求物件前,必須設置URL、標頭、請求參數、本體內容等,在這當中,有些參數是必要,有些參數則可以有預設值,能選擇性地設置,以便建立不同風格或功能的請求物件。

面對這類需求,我們在過去經常採用Builder模式,然而,在某些程式語言中,具名可選參數或選項物件之類的模式,會是更好的方式嗎?

Java與Builder模式

在Java語言中,若構造物件時,必須提供必要與可選參數,由於Java本身並不提供具名可選參數(Named optional parameters)語法,為了令建構式可以有多種建構風格,設法重載出多個建構式是方式之一。

然而,對於建立HTTP請求物件的這類需求來說,由於可選參數繁多,為此須提供數個建構式,在實作上的負擔頗大,而且,客戶端在運用時,關於程式碼的撰寫上,容易出錯、也難以閱讀,因此,有不少文件或書籍,也都會談到下列狀況:出現這類重疊建構式(Telescoping constructor),其實是個反模式。

解決的方式之一,是在建構式上接受必要參數,可選參數則在建立物件之後,透過可變動物件狀態的方法來設置(像是Setter方法)。基本上,採用這種模式的好處是實作簡單,由於方法具有名稱,客戶端在建構與設置物件上,也很容易,具有較高的可讀性。

雖然上述作法解決了重疊建構式的問題,然而,可選參數之間可能會有關聯。因此,這種方式的問題就在於,整個物件構造過程被拆成了數個階段,難以保證物件確實地執行了必要的階段,也就難以保證物件最後是在可用狀態;另一方面,這樣的物件顯然是狀態可變的,如果最後想要的是不可變物件,還要想辦法基於最後的物件狀態,產生一個不可變的物件版本。

在Java中,通常會建議採用Builder模式來解決這類需求。而Builder模式乍看與工廠方法的意圖類似,都是要分離物件的建構流程與使用流程,不過,工廠方法之目的,在於封裝物件的建構過程,相對地,Builder模式則允許客戶端控制建構過程的每一步,並決定何時真正建立物件。OkHttp建立請求物件的方式,就是一個例子:

Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.build();

通常Builder模式會在每個收集參數的方法執行過後,傳回自身、形成流暢風格,因而提高可讀性;雖然Builder物件本身是可變的,但是,在build方法執行時,才會建構物件,因而最後可以建構出不可變物件;必要的話,我們可以在收集參數的過程中,先行檢查參數的合法性,最後,在build當中,也可以檢查物件的狀態,當物件處於非法狀態時,拋出IllegalArgumentException,並說明哪些參數設置無效。

Kotlin的具名可選參數

在《Effective Java》第三版條目二,就推薦使用Builder模式,來取代重疊建構式,其中,也提到「Builder模式模擬了具名可選參數」。那麼,在提供具名可選參數的語言中,是否就不需要Builder模式呢?

以同為JVM上可運作的語言為例,Scala就具有具名可選參數,有不少文件也指出,Builder模式是個舊模式。

而在Kotlin這方面,也有一些開發者特意探討《Effective Java》的條目,思考在Kotlin中是否需要做出調整或棄用,像是〈Effective Java in Kotlin〉系列文件於條目二的探討中(https://bit.ly/2SKQyQu),就談到不需要模擬Builder模式,因為,在Kotlin中,我們可以直接使用具名可選參數,例如,同樣建立請求物件的話,在Kotlin當中,我們可以這麼做:

Request request = new Request(
url = "https://api.github.com/repos/square/okhttp/issues",
userAgent = "OkHttp Headers.java");

甚至,在〈Avoiding the Builder Design Pattern in Kotlin〉(https://bit.ly/2HlNw3h)文件,就直接在標題表示避免使用Builder模式,而內文更提到,Builder模式甚至被某些開發者認為是反模式。

JavaScript的選項物件

如果使用Builder模式之目的,只是想要擁有具名可選參數的效果,那麼,在擁有具名可選參數的語言中,確實就不需要使用Builder模式了;不過Builder模式之真正目的,並非只用來模擬具名可選參數,Joshua Bloch只是在提到Builder模式的可讀性時,談到它能模擬具名可選參數罷了。

正如方才所言,允許客戶端控制建構過程的每一步,也是Builder模式之目的,例如,於流程的多個階段中收集資源(例如StringBuilder),而在,這個過程中物件尚未真正建立,因而沒有狀態不一致的問題;Builder模式也可以抽象化,《Effective Java》就示範了,如何讓抽象類別有抽象的Builder,具體類別有具體的Builder實作。

另外,具名可選參數也沒有解決長參數的問題。想像一下,若可選參數達十幾個以上,就算用了預設引數,客戶端在使用上的可讀性,也許還能接受,然而,隨之而來的是,類別在實作建構式上,卻有十幾個參數必須撰寫,這樣的方法簽署樣貌,看來也是十分可怕。

如果程式語言本身有適當的支援語法,我們能使用選項物件(Option object)來收集參數,並令建構式接受選項物件,就可以解決這類長參數的問題,也具有一定的可讀性,例如,在JavaScript中就經常採用這種做法,以Fetch API為例:

fetch('upload', {
method : 'POST',
body : formData
});

Builder模式確實也容易拿來與選項物件互相比較,像是在〈Builders vs option maps〉(https://bit.ly/2tRJCqB)就談到了,如果語言同時支援兩者,而使用Builder可以做到的事情,選項物件也能做到的話,選項物件會是比較建議的做法。

思考多風格物件的需求

當然,就像在《Effective Java》等文件中都提過的,Builder模式使用上有其負擔,像是必須另外創建Builder角色的類別,幸而這方面有工具可以協助,像是透過Lombok、AutoValue之類的程式庫,來修改既有類別,或者自動產生Builder類別原始碼,而現行的各種整合開發工具,像是NetBeans、Eclipse(透過plugin)中的重構工具,也可以將建構式重構為Builder。

的確!具名可選參數、選項物件等,可以在某些場合取代掉Builder模式,然而,實際上並不是哪個語言中應該使用哪個模式,或者哪個語言中該棄用哪個模式的問題,而應該將具名可選參數、選項物件等,作為一種思考的過程。如果它們可以更好地解決問題,就使用(例如Java 9之後,或許可以透過Map.of來取代某些Builder模式之使用),若最後還是Builder適用,就使用Builder!

作者簡介


Advertisement

更多 iThome相關內容