現在不少程式庫在使用風格上,採用了方法鏈串(Method chaining)創造出流暢語義,多數人耳熟能詳的實例就是jQuery,而Guava程式庫中,如ComparisonChain、Ordering,也採用了此風格,有些程式庫改版時亦從命令查詢(Command-Query)風格,丕變為流暢API風格,像是Quartz,此類風格似乎打破了一些設計原則?流暢API設計,就等於方法鏈串嗎?

命令查詢分離概念
在探討流暢API設計之前,得先來看看命令查詢分離(Command Query Separation)概念,這在Martin Fowler的2005年文章〈CommandQuerySeparation〉中曾經提過。

它的基本概念,是將物件的方法分為命令與查詢兩類,改變系統狀態但不傳回值的方法稱為命令,傳回結果但不改變系統狀態的方法稱為查詢。在JDK中,Calendar類別具備命令查詢分離概念,其set、add、roll等方法變動Calendar實例的方法,傳回型態都是void,而查詢結果的get等方法,並不改變物件狀態。

Martin Fowler提到,採用此原則的益處是,對於一個狀態可變的物件來說,可以清楚地辨別哪些操作會有副作用(Side effect)而哪些不會,你可以將物件傳遞給需要查詢的場合,而不用擔心會改變狀態;另一個好處是,查詢方法的傳回型態可洩露物件命令操作後的差異性,以Iterator的next方法為例,Martin Fowler個人不喜歡next同時傳回下個項目,並推進Iterator,依命令查詢分離概念將之分開為advance與current方法,會是他偏好的設計方式。

命令查詢分離概念的優點,是方法的職責清楚(後來又演變出CQRS模式,是有關物件的查詢與命令職責分離,Martin Fowler在2011年的〈CQRS〉討論過),但缺點是有時須透過一連串的命令操作,方可獲得想要的物件狀態,容易形成冗長程式碼。

以Calendar類別為例,若想知道1975年5月26日五天後的六個月又三星期,是什麼日子,在透過Calendar.getInstance()取得Calendar實例後,得進行以下的冗長命令操作:

calendar.set(1975, Calendar.MAY, 26, 0, 0, 0);
calendar.add(Calendar.DAY_OF_MONTH, 5);
calendar.add(Calendar.MONTH, 6);
calendar.add(Calendar.WEEK_OF_MONTH, 3);

方法鏈串/不可變物件的連續技
命令查詢分離在概念上,是將變更物件狀態的命令與物件狀態的查詢在方法職責上加以分離,如果物件不可變(Immutable)呢?那麼在操作後,你勢必得傳回某個值,如果該值就是物件,那就可以直接對傳回的物件進行操作,形成鏈狀操作。Java的字串就是個例子,因為String實例不可變,因而可以進行如下的鏈結操作:
param.trim()
.toLowerCase()
.replace(regx, mask);

這會比在每個方法操作後,使用變數接受結果,再透過變數進行下一操作來得簡潔,且不違反命令查詢分離概念,因為你並沒有改變物件狀態,而是基於目前物件狀態,(也就是查詢後)建立新物件。

對創造流暢的可讀性而言,不可變物件結合方法鏈串,似乎是絕佳組合,例如作為打算取代Calendar與Date的Joda-Time程式庫,或JSR310來說,就利用了方法鏈串,來改進日期運算時的程式碼流暢度,像是同樣想知道,1975年5月26日五天後的六個月又三星期,是什麼日子,利用JSR310,可寫為LocalDate.of(1975, 5, 26).plus(5, DAYS).plus(6, MONTHS).plus(3, WEEKS),相較於Calendar的操作簡潔許多。在我先前專欄〈排序處理模式〉中,談到了Guava的Guava的Ordering實作,也利用方法鏈串,讓不可變的Ordering在建構流程一目瞭然。

Quartz在2.0之後,實際上就是採用此模式,例如建立Trigger時,會建立TriggerBuilder以設定組態,TriggerBuilder不可變,每個組態操作都建立了新的TriggerBuilder實例,設定完成後,才從TriggerBuilder的build方法,產生想要的Trigger物件。

流暢介面/可變物件的連續技

命令查詢分離是個概念,Martin Fowler說過在可以的情況下儘量遵守,但在語義明確的情境下,不用墨守成規。他以堆疊的pop方法為例,pop很明確地表示「取出」堆疊頂端物件,雖然它同時改變物件狀態並傳回堆疊頂端物件,這種違反的情況他倒是樂於接受。

Martin在2005年另一篇〈FluentInterface〉文中,也談到了另一個情況:一個Order物件需要在多行程式碼中多次執行addLine方法,他建議改以流暢風格設計為customer.newOrder().with(6, "TAL").with(5, "HPK").skippable().with(3, "LGV").priorityRush()。

建立這個風格,導致一些不尋常的習慣,會改變Order物件狀態的with方法傳回了本身(this),明顯違反了命令查詢分離概念,然而基於流暢的可讀性優點下,Martin Fowler建議暫且忽略這個習慣。Martin Fowler將這風格命名為流暢介面(Fluent Interface)。而Hibernate中的Criteria API亦採用此風格,你可以用session.createCriteria(Cat.class).add(like("name", "Iz%")).add(gt("weight", new Float(minWeight))).addOrder(asc("age"))來建立查詢條件。

流暢介面風格常見以方法鏈串實作,適合用於組態設定或者建立值物件,在適當的傳回值型態與方法命名下,值物件的建立情境會形成特定語言,也就是一個內部DSL(Internal Domain Specific Language),例如JavaScript中一個有趣專案是JSVerbalExpressions,可使用VerEx().startOfLine().then('http').maybe('s').then('://').maybe('www.').anythingBut('').endOfLine()來建立規則表示式(Regular expression),讓規則表示式的建立不再隱晦不明。Martin Fowler說到,這種流暢API的設計,也是對靜態語言在DSL上的一個補充。

可讀性是流暢API出發點
除了打破命令查詢分離概念來創造可讀性,還可進一步考慮合併命令與查詢方法,用以簡化API。

例如jQuery的css方法既是命令方法,像是$('#some').css('color', 'red'),也可以是查詢方法,像是$('#some').css('color'),在Java可用重載(Overload)方法達到相同目的,就某些程度來看,就像是合併取值(Getter)與設值(Setter)。

在考量到可讀性之後,實作的形式就多元化了。實際上,流暢介面與流暢API之間不畫上等號。

方法鏈串是流暢API常見但不是唯一實現方式,Martin Fowler在2008年對〈FluentInterface〉的補充說明了這點,像是Hamcrest就採用工廠方法等模式,實現了assertThat(numbers, everyItem(lessThan(5)))之類的風格。然而,不用拘泥於名詞,流暢API的重點或許在思考:打破原則或概念是可以創造可讀性,分寸在哪呢?

大部份設計原則考量的都是職責清晰,降低耦合度,必定優先遵守,以命令查詢分離概念來說,設計API時應該優先考量,其次在一些語義明確或慣例的情境下,若有助於可讀性則可考慮通融,像是堆疊的pop方法。

如果物件不可變,由於操作總得傳回結果,你可以傳回新物件,以方法鏈串創造流暢性;如果物件可變,且是個值物件,考慮傳回本身,讓查詢方法傳回型態來洩露命令操作的差異,並明確在文件上說明傳回型態是否為本身,作為API使用者也務必確認這點;如果目標是構成一個內部DSL,那在型態與方法名稱上,就得多下點功夫,一切都以可讀性為出發點,API才有流暢的可能性。

專欄作者

熱門新聞

Advertisement