Option Object

December 24, 2021

在〈Builder(Effective Java)〉談到,如果你的 API 具有冗長參數,程式碼可能散發著餿味(bad smell),在 Java 領域想解決這個問題,往往會形成 Builder 這類的模式。

Python 的 dict

不過在某些語言中,由於語言的特性,你可能很少看到 Builder 模式之類的實現,例如 Python,若參數個數很多:

def request(url, method, contents, datatype, accept, headers, username, password):
	... 建立請求物件並傳回

這樣的函式定義不但醜陋,呼叫時也很麻煩,單純只搭配關鍵字參數或預設引數,也不見得能改善多少,將來若因需求而必須增減參數,也會影響函式的呼叫者,因為改變參數個數,就是在改變函式簽署(Signature),也就是函式的外觀,這勢必得逐一修改影響到的程式,造成未來程式擴充時的麻煩。

這個時候,可以試著將選項收集為一個 dict。例如:

def request(url, option):
    settings = {
        'method' : option.get('method', 'GET'),
        'contents' : option.get('contents', ''),
        'datatype' : option.get('datatype', 'text/plain'),
        ... 其他設定 
    }
    
	... 建立請求物件並傳回

option = {'method' : 'POST', 'contents' : 'book=python'}
req = request('https://openhome.cc', option)

JavaScript 物件實字

若是 JavaScript 有著以下冗長參數的函式:

function request(url, method, contents, datatype, accept, headers, username, password) {
	...建立請求物件並傳回
}

由於 JavaScript 物件實字撰寫簡易,很常看到用以下的方式來解決冗長參數的問題:

function request(url, option) {
	const settings = {
        method : option.method || 'GET',
        contents : option.contents || '',
        datatype : option.datatype || 'text/plain',
        ...其他設定 
    };

	...建立請求物件並傳回
}

const req = request('https://openhome.cc', {
	method: 'POST',
	contents: 'book=python'
});

採用此模式的 API 很多,例如 Fetch API:

const promise = fetch('https://openhome.cc', {
	method : 'POST',
	headers : {
		'Content-Type' : 'application/x-www-form-urlencoded'
	},
	body : reqString
});

若要建立物件時,可指定的選項很多,想讓選項的指定簡化或易理解,這類將選項收集在一個物件的模式,稱為 Option Object,若語言在建立物件收集選項時的語法很便利,往往會採用這個模式。

Java 不是也有 Map 嗎?當然,如果鍵/值型態單純的話,使用 Map 也可以,例如鍵/值都是 String 的情況,然而若是選項上需要各種形態時,Map 就不方便了,畢竟 Java 是靜態定型語言,處理 cast 等問題會煩死人,因此 Option Object 這種模式,看似較少在 Java 的領域中看到。

不過呢!就作用而言,〈Builder(Effective Java)〉中的 Builder,作用是收集選項,最後傳給了 Request 作為建構之用,這時 Builder 的角色,不就是 Option Object 嗎?作用上類似,只是實作上麻煩一些。

模式是語言缺陷?

談到這個,有不少人認為,模式暗示、代表或其實就是語言的缺陷,像是用此論調,討論 Builder 模式代表著 Java 語言沒有預設引數之類的缺陷…唉…是這樣的嗎?

先不討論「模式是語言缺陷」這件事,說 Builder 模式會在 Java 流行,是因為沒有預設引數的話,這本身就不對了,之前一直在說的,預設引數本身並沒有解決冗長參數的問題…而且概念上,身為 builder 角色的物件,最後與 Option Object 的作用類似,只是實作上麻煩一些,而且你怎麼不說,JavaScript 有 Option Object 模式,因此 JavaScript 在這方面有缺陷呢?

如果模式代表語言的缺陷,這就好像在說存在一門語言,用它來撰寫的話,每個人寫出來的設計、風格絕對不會構成相似性,不會有任何模式出現,有這種語言的可能性嗎?

從另一個角度來看,組合語言會使用 CMPJMP 等指令做出判斷、重複之類的設計,這就像是判斷、重複等,就是組合語言中的模式,組合語言沒有 if、for 等特性,這是組合語言的缺陷…呃…好像不能這麼說吧!

任何語言在使用時,必須是基於其語法、特性等,從事更高階的設計,而這些設計從事的人多了,就必然會形成某種模式。

有些模式最後會構成程式庫或框架,從某些程度上,程式庫或框架就是語言的擴充,有些語言確實也提供語法特性,可以讓開發者在設計程式庫或框架時,像是在構造語言中的微語言,讓程式庫或框架在使用時像是內建語法一樣地自然,有些語言可以透過巨集,讓你直接擴展語言。

有些模式在某些語言中是隱含的,有些模式用某些語言實作時會比較簡單,例如,在具有一級函式的動態定型語言中,某個函式其實就是 factory 的概念,物件間的協定也不需要使用 interface 來定義,鴨子定型就可以了。

有些模式確實後來會可能在語言的新版本中,以新語法的需求出現,有人會說「看吧!就說是缺陷,這語言不是補上了嗎?」嗯…若認為模式是語言缺陷,語言的新版本用新語法就是補上缺陷,那麼總有一天,方才談到的完美語言會出現嗎?

絕對不會!因為在不斷因模式出現而彌補缺陷的過程中,這語言就會逐漸被不斷新增的特性給壓垮,越來越多的特性,造成學習曲線越來越高、選擇越來越多、程式碼越來越複雜、越來越看不懂、越來越難以維護…然後越來越多的人遠離該語言…直到有一日該語言迎來終焉…RIP…

不要因為長期以來,使用的語言一直難以解決你某些需求,看到另一門語言簡單幾行就能解決掉,就開心地擁抱另一門語言,然而該語言可能在面對其他需求時,有更多難以施力之處,只是一時因為長期以來的積怨確實獲得宣洩,你忘了、沒時間或不想去檢視那些需求罷了?

模式代表的是一群人在目前語言的抽象上,建立起來的相似性,相對地,也就是代表語言在特性方面的取捨,許多人會說模式是缺陷,其實應該是在反對,不假思索地套用模式,過於重視模式的名稱、實現形式,將程式碼寫到像某種模式作為終點,而不是將之視為思考過程的參考之一。

你應該試著從模式中,看看語言原本做了哪些取捨,目的是為了發揮現有的哪些特性,從而知道在面對、解決需求時,人們在目前語言的抽象上,是什麼樣的過程與想法,才會建立起某些相似性,構成了某些模式,透過這些模式作為引導,思考需要作哪些考量,才能更適切地解決需求,這才是面對模式的正確姿勢吧!