流暢介面
2005 年 12 月 20 日
幾個月前,我參加了 Eric Evans 的工作坊,他談到一種特定風格的介面,我們決定將其命名為流暢介面。這並非常見的風格,但我們認為應該讓更多人知道。或許最好的說明方式就是舉例。
最簡單的範例可能來自 Eric 的 timeAndMoney 函式庫。要以一般的方式建立時間間隔,我們可能會看到類似這樣的程式碼
TimePoint fiveOClock, sixOClock; ... TimeInterval meetingTime = new TimeInterval(fiveOClock, sixOClock);
timeAndMoney 函式庫使用者會這樣做
TimeInterval meetingTime = fiveOClock.until(sixOClock);
我將繼續使用常見的範例,為客戶建立訂單。訂單有明細項目,包含數量和產品。明細項目可以跳過,表示我寧願不含此明細項目出貨,也不要延遲整個訂單。我可以將整個訂單設定為緊急狀態。
我看到建立這種東西最常見的方式如下
private void makeNormal(Customer customer) { Order o1 = new Order(); customer.addOrder(o1); OrderLine line1 = new OrderLine(6, Product.find("TAL")); o1.addLine(line1); OrderLine line2 = new OrderLine(5, Product.find("HPK")); o1.addLine(line2); OrderLine line3 = new OrderLine(3, Product.find("LGV")); o1.addLine(line3); line2.setSkippable(true); o1.setRush(true); }
基本上,我們會建立各種物件並將它們連接在一起。如果我們無法在建構函式中設定所有內容,則需要建立暫時變數來協助我們完成連線,特別是在將項目新增到集合中的情況下。
以下是使用流暢風格完成的相同組裝
private void makeFluent(Customer customer) { customer.newOrder() .with(6, "TAL") .with(5, "HPK").skippable() .with(3, "LGV") .priorityRush(); }
關於此風格,最重要的注意事項可能是,其目的是執行類似於內部 特定領域語言 的操作。事實上,這就是我們選擇「流暢」一詞來描述它的原因,在許多方面,這兩個詞是同義詞。API 主要設計為可讀且流暢。這種流暢性的代價是更多的精力,無論是在思考還是 API 建構本身。建構函式、設定值和新增方法的簡單 API 容易撰寫許多。想出一個好的流暢 API 需要思考很多。
事實上,這個小範例的問題之一是我只是在卡加利的咖啡廳裡在早餐時隨手寫的。好的流暢 API 需要花時間建構。如果你想要一個經過深思熟慮的流暢 API 範例,請查看 JMock。JMock 與任何模擬函式庫一樣,需要建立複雜的行為規範。過去幾年來已經建構了許多模擬函式庫,JMock 包含一個非常好的流暢 API,流暢度極佳。以下是一個預期範例
mock.expects(once()).method("m").with( or(stringContains("hello"), stringContains("howdy")) );
我在 JAOO2005 看見 Steve Freeman 和 Nat Price 針對 JMock API 的演進發表精彩演說,他們後來在 OOPSLA 論文 中撰寫了相關內容。
到目前為止,我們大多看到流暢的 API 用於建立物件組態,通常涉及值物件。我不確定這是否是一個定義特徵,儘管我懷疑它們出現在宣告式內容中是有原因的。對我們來說,流暢性的關鍵測試是特定領域語言的品質。使用 API 的方式越像語言流暢,就越流暢。
建立像這樣的流暢 API 會導致一些不尋常的 API 習慣。最明顯的習慣之一是會傳回值的設定程式。(在 order 範例中,with
會將訂單明細新增到訂單並傳回訂單。)在花括號世界中,常見的慣例是修改器方法為 void,我喜歡這種慣例,因為它遵循 CommandQuerySeparation 原則。此慣例確實會妨礙流暢介面,因此我傾向於暫停此慣例。
您應該根據繼續流暢動作所需的內容選擇傳回類型。JMock 會根據接下來可能需要的內容移動其類型。這種樣式的其中一個好處是方法完成 (Intellisense) 有助於告訴您接下來要輸入什麼內容,就像 IDE 中的精靈一樣。一般來說,我發現動態語言更適合 DSL,因為它們的語法往往比較簡潔。然而,使用方法完成對於靜態語言來說是一個優點。
流暢介面方法的問題之一是它們本身沒有太多意義。檢視方法瀏覽器中方法的逐項文件,並不會對 with
顯示太多意義。的確,單獨放在那裡,我會認為它是一個命名不佳的方法,根本無法傳達其意圖。只有在流暢動作的背景下,它才會展現其優點。解決此問題的方法之一可能是使用僅在此背景下使用的建構函式物件。
Eric 提及的一件事是,到目前為止,他主要在值物件的組態中使用並看到流暢介面。值物件沒有具有領域意義的身分,因此您可以輕鬆地建立並丟棄它們。因此,流暢性取決於從舊值建立新值。從這個意義上來說,順序範例並非典型,因為它是 EvansClassification 中的實體。
我尚未看到許多流暢介面,因此我得出結論,我們對它們的優缺點了解不多。因此,任何使用它們的勸告只能是初步的 - 不過,我確實認為它們已成熟,可以進行更多實驗。
有一個很好的後續文章來自 Piers Cawley。
更新(2008 年 6 月 23 日)。自從我寫這篇文章以來,這個術語已被廣泛使用,這讓我有種令人興奮的滿足感。我在 我一直在撰寫的書 中改進了我對流暢介面和內部 DSL 的想法。我也注意到一個常見的誤解 - 許多人似乎將流暢介面等同於方法串接。串接當然是與流暢介面一起使用的常見技術,但真正的流暢性遠不止於此。
我在上面展示的 JMock 範例使用了方法串接,但也使用了巢狀函式和物件範圍