當面對的問題是要解決底層細節時,可採用面對底層平臺或機器的低階思維來解決;當解決方案出現重複性,抽取重複的部份以進行重用,這就開始隱藏了某些底層細節,也就是一定程度脫離了低階層次。

程式碼或結構等的重複是較易觀察的,然而語意或商務領域上的重複行為則不易察覺,能從改良可讀性開始,進而察覺每次處理時的重複意圖,從中建立更高階的處理語意。

現代語言搭配的程式庫,多半提供了排序演算法的實作,如何實作是低階細節,實際上演算法執行流程可以共用,排序過程取得兩元素進行順序比較時,兩元素前後順序的資訊則必須由使用者提供。

以Java為例,可定義物件與生俱來的順序(Natural ordering),也就是定義類別時實作Comparable介面,如果不打算使用或無法定義物件與生俱來的順序(例如不打算或無法修改原始碼),那麼也可以建立比較器,也就是另行實作Comparator介面,並在需要排序的場合,使用指定的比較器。

當有A、B兩個元素進行順序比較時,有三個可能性,A順序在B之前、A與B順序相同、A順序在B之後,通常使用者必須實作函式或方法來告知這三者之一,為了使傳回值能讓排序演算判斷,多數程式庫要求使用者須傳回整數型態的-1、0或1。

以Java為例,無論是Comparable的compareTo,或者是Comparator的compare,傳回值必須是int型態的-1、0或1,然而許多開發者經常搞錯這三個整數實際對應的意義。

想進行高層次語意封裝的第一步,就是改進可讀性,若將A當作左值而B當作右值,在想要相容於現有程式庫的情況下,可定義int變數LEFT_IS_GREATER、RIGHT_IS_GREATER分別代表1與-1,這就可以讓比較後傳回值的意圖明顯許多。

有些訴求高階語意的語言,像是Haskell,就定義有Ordering型態,其有LT、EQ與GT三個值,在需要比較的場合,可明確表示程式碼的意涵與結果。

抽取可重用的排序模式
有些語言本身就具有高階概念,純函數式語言以宣告式(Declarative)風格來撰寫程式,只不過談到函數式,總讓習慣命令式(Imperative)的開發者望之卻步,其實像是SQL也是宣告式語言。

就排序這方面來說,如果SELECT的結果打算先依姓來排、再依名來排,如果姓名相同再依郵遞區號來排,只要使用ORDER BY子句來指定就可以了。如果姓、名、郵遞區號是某物件的屬性,無論是實作Comparable或Comparator,那麼compareTo或compare中的程式碼,基本上並不容易閱讀。

實際觀察程式碼中每個屬性的比較處理,會發現有個模式,亦即都是在兩個屬性不相等時直接return傳回結果,相等時才進行接下來其他屬性的比較,透過設計將這流程封裝,就可建立類似宣告式的風格。例如,Guava的ComparisonChain,就可以撰寫出以下的宣告式風格:

ComparisonChain.start()

 .compare(lastName, other.lastName)

 .compare(firstName, other.firstName)

 .compare(zipCode, other.zipCode).result();

這可以用來實作Comparable與Comparator的compareTo及compare,對於Comparator,可觀察幾個常見實作,像是依物件與生俱來的順序或反序實作、依物件字串描述比較、基於某運算式結果來排序等。

仔細想想,字串與生俱有辭彙順序(Lexicographic ordering),而反序則只需將原比較器要比較的兩值對調即可,也就是說條件複雜一些的比較器,可以基於某個既有的比較器組裝而來。

先前在〈辨識物件的可裝飾行為〉中談過,一旦這類行為辨識出,可用裝飾器(Decorator)模式加以抽取,如此就能動態對行為擴充功能。

建立高階處理語意

單純只是抽取出可裝飾行為,以裝飾器模式加以實作,基本上只是觀察出重複的程式碼或結構,某些程度上已脫離了低階層次的處理,然而語法及組裝方式上還是機器的思維模式。

例如,想根據某物件屬性排序,若為null,優先排在前頭,若非null就依物件與生俱來的順序反向排序,若單純使用裝飾模式實現的話,結果會產生如new OnResultOf(Person p -> p.field == null ? null : p.field, new NullsFirst(new Inverse(new Natural())))。

首先,new本身是建立物件的概念,這是從JVM角度看待,摻雜了建構物件概念的程式碼,加上括號語法的干擾,這並非易於理解的語法。

談到建構語意,先前我在〈運用工廠、回呼與鞣製自訂語意〉中提到,鞣製是可用的實作方向之一。仔細想想,OnResultOf、NullsFirst、Inverse、Natural若都實作了Comparator,且裝飾的職責由物件本身負責,也就是定義為物件的方法之一,那麼每次方法產生的物件也是Comparator,就能用來實現鞣製概念,創造方法操作鏈來建立語義。

Guava的Ordering就是如此實作,Ordering實作了Comparator,本身定義了natural、reverse、nullsFirst、onResultOf等方法,每個方法的傳回實例都是Ordering型態,裝飾行為實現在各個方法之中。例如reverse的實作就是傳回new ReverseOrdering< S >(this),因此,就可以使用Ordering.natural().reverse().nullsFirst().onResultOf(Person p -> p.field == null : p.field)操作鏈,建構出想要的Ordering實例。

使用Guava的Ordering建構複雜的比較器時,與採用純裝飾器時的思路相同,都是從一個基本的比較器開始,逐步裝飾以得到最後想要的比較器。只不過以純裝飾器模式實作時,語法上基本裝飾器會位於右邊,逐一在左邊裝飾上其他裝飾器,也就是想得知裝飾器建立過程時,必須由右往左閱讀。使用Guava的Ordering時,則是由左往右組裝。

有些程式庫也是這樣,像是Joda-Time的DateTime,就程式碼而言,從左邊一個基本的DateTime開始,往右逐步產生想要的DateTime物件。

Guava的Wiki對Ordering的說明指出,Ordering的操作鏈閱讀時應由右往左,才能瞭解排序結果,這反而令人不易瞭解;人類閱讀基本上是由左而右,Ordering的操作鏈由左而右的閱讀,目的就是讓人類以自然的方式,瞭解Ordering實例如何組裝。

從意圖中察覺重複模式
Guava的Ordering以鞣製概念,製造出方法操作鏈來創造語意,留下的問題是,Ordering上該有哪些方法?若不能辨識出最通用的方法再定義在Ordering上,最後Ordering將負過多的職責。Ordering上定義了最通用的如natural、reverse、nullsFirst等,當然,通用的定義每個人認知不同,為了提供更多建構比較器的彈性,Ordering提供from、compound等方法,讓使用者有更多組裝可能性。

思考問題時,可以通用語言來定義問題,而不是受限於語法或低階的模型,我在〈List處理模式〉中談過,當時舉List處理時常出現的模式為例,並從處理流程中抽出重複模式,發現面對資料管理問題時,幾乎都可用map、filter、fold概念。

上述以排序時實作的Comparable與Comparator,也是類似道理,有時應當從意圖中察覺重複模式,而不只是從程式語法中察覺重複模式,當我們一再寫出new OnResultOf(Person p -> p.field == null ? null : p.field, new NullsFirst(new Inverse(new Natural())))的語法時,就應思考我們的意圖就是從基本元件開始,逐步組裝出想要的功能。

專欄作者

熱門新聞

Advertisement