iThome 網站首載:排序處理模式
當面對的問題是要解決底層細節時,可採用面對底層平台或機器的低階思維來解決;當解決方案出現重複性,抽取重複的部份以進行重用,這就開始隱藏了某些底層細節,也就是一定程度脫離了低階層次;程式碼或結構等的重複是較易觀察的,然而語意或商務領域上的重複行為則不易察覺,可從改良可讀性開始,進而察覺每次處理時的重複意圖,從中建立更高階的處理語意。
Comparable
與Comparator
現代語言搭配的程式庫多半提供了排序演算法的實作,如何實作排序演算法是低階細節,實際上演算法執行流程可以共用,排序過程取得兩元素進行順序比較時,兩元素前後順序的資訊則必須由使用者提供。以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也是宣告式(Declarative)語言。就排序這方面來說,如果
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();
ComparisonChain
的概念,可以用來實作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())))
的語法時,就應思考我們的意圖就是從基本元件開始,逐步組裝出我們想要的功能。