Java FP: Java中函數式編程的謂詞函數(Predicates)第二部分
在上一篇文章中我們介紹了謂詞函數。通過一個簡單的隻帶一個返回值是true或者false的函數的接口,把函數式編程語言的優勢帶入到了類似Java的麵向對象編程語言中。這一小節,我們將會介紹一些高級特性,方便你高效利用謂詞函數。
測試
在測試代碼中使用謂詞的優勢尤為明顯。當你需要測試一個混合了數據結構與某些條件邏輯的方法時,通過使用謂詞,你可以先單獨測試數據結構,再測試條件邏輯。
第一步,先利用永真謂詞或者永假謂詞屏蔽用於判斷的邏輯,將注意力集中在測試數據結構上:
1 |
// check with the always-true predicate |
2 |
3 |
final Iterable<PurchaseOrder> all = orders.selectOrders(Predicates.<PurchaseOrder> alwaysTrue());
|
4 |
5 |
assertEquals( 2 , Iterables.size(all));
|
6 |
7 |
// check with the always-false predicat |
8 |
9 |
eassertTrue(Iterables.isEmpty(orders.selectOrders(Predicates.<PurchaseOrder> alwaysFalse()))); |
第二步,你可以分開測試每個謂詞了:
1 |
final CustomerPredicate isForCustomer1 = new CustomerPredicate(CUSTOMER_1);
|
2 |
3 |
assertTrue(isForCustomer1.apply(ORDER_1)); // ORDER_1 is for CUSTOMER_1
|
4 |
5 |
assertFalse(isForCustomer1.apply(ORDER_2)); // ORDER_2 is for CUSTOMER_2
|
例子簡單,卻見微知著。為了測試更加複雜的邏輯,你可能需要構建模擬謂詞應對測試不夠充分的情況。比如,一個第一次返回true,之後都返回false的謂詞。通過嚴格分離關注點,謂詞可以大大簡化你的測試計劃。
如果你想測試驅動開發(TDD),謂詞是一個不錯的選擇,如果你能通過測試的方式影響你的設計,之後你便會發現謂詞很容易就融入到你的設計中。
向你所在的團隊推廣謂詞函數
在我工作過的項目組中,一開始團隊總是不熟悉謂詞函數。然而謂詞的概念是非常有趣的,也很容易讓其他團隊成員所接收。事實上,我並沒有肆意宣傳,謂詞的思想在我往同事的代碼裏添加了代碼之後便悄悄傳播開來,對此我很驚訝。我猜使用謂詞函數的好處不言自明,一些比較成熟的類似Apache和Google的API同樣證明了謂詞是一個比較嚴肅的話題,而如今正是函數式編程最火爆的時期,謂詞函數這類函數式語言的特性更加容易推銷了。
簡單的優化
為了防止謂詞在線程間共享數據,盡可能將謂詞設計成不可變和無狀態是常規優化手段之一。這項優化可以使整個進程共享一個謂詞實例(單例,比如一個謂詞常量)。如果有需要,可以將編譯期間無法枚舉出來的最常用的謂詞在運行時緩存起來(同往常一樣,隻有在性能分析報告中真正要求了緩存之後,你才需要做這項工作)。
如果可以的話,盡可能將計算所需的資源提前在構造函數中加載好(這是線程安全的操作),否則使用懶加載的方式。
謂詞應當是“無副作用的”,換句話說,是隻讀的:謂詞的執行不應該修改係統內的任何狀態。某些比如基於計數的分頁謂詞必須維護自己的內部狀態,但是它們依然不能更改所依賴的係統狀態。除非內部狀態可以在每次調用之間被重置,其他情況下這些狀態是不能被公用的。
細粒度接口:讓你的謂詞函數擁有更多的使用者
在大型應用程序中,你發現自己經常為完全不同的主體類型編寫了引用類似Customer的通用屬性的相似謂詞。比如,在管理員頁麵,你想過濾出某個客戶的日誌,在CRM頁麵,你想過濾出某個客戶的吐槽。對於每個特定類型X,你同樣需要一個相關聯的謂詞CustomerXPredicate進行客戶級別的過濾。因為每個類型在某些方麵都與Customer關聯,我們可以將Customer抽離出來,把它聲明在隻有一個方法的CustomerSpecific 接口裏:
1 |
public interface CustomerSpecific {
|
2 |
3 |
Customer getCustomer();
|
4 |
5 |
} |
這個接口除了不能重用實現類,細粒度的特點讓我想起在某些語言特征。接口使得getCustomer() 方法能夠做到無視類型調用,所以同樣可以將其看做是在靜態類型語言中引進動態類型的方式。
一旦我們定義了CustomerSpecific接口,我們可以通過實現它定義我們的謂詞,不需要像之前一樣根據主體類型定位謂詞。這項優化在一個大型項目中僅僅隻是影響了一小部分的謂詞類。在這種情況下,CustomerPredicate通過實現CustomerSpecific進行重定位,並且泛型類型也是CustomerSpecific。
01 |
public final class CustomerPredicate implements Predicate<CustomerSpecific>, CustomerSpecific {
|
02 |
03 |
private final Customer customer; // valued constructor omitted for clarity
|
04 |
05 |
public Customer getCustomer() {
|
06 |
07 |
return customer;
|
08 |
09 |
}
|
10 |
11 |
public boolean apply(CustomerSpecific specific) {
|
12 |
|
13 |
return specific.getCustomer().equals(customer);
|
14 |
15 |
}
|
16 |
17 |
} |
注意到例子中的謂詞可以實現CustomerSpecific,所以同樣可以針對自身做evaluate計算(譯者注:即調用謂詞的apply或者evaluate等方法)。當使用這類特征模擬(譯者注:在靜態語言中模擬動態語言特有的特性)的接口時,需要特別注意對泛型的設計,比如在類PurchaseOrders中接收Predicate<PurchaseOrder>作為參數的方法,需要稍作修改,使該方法能夠接收PurchaseOrder任何父類的謂詞:
1 |
public Iterable<PurchaseOrder> selectOrders(Predicate<? super PurchaseOrder> condition) {
|
2 |
|
3 |
return Iterables.filter(orders, condition);
|
4 |
5 |
} |
域驅動設計(Domain-Driven Design)規範
Eric Evans和Martin Fowler一起寫了《模式規範》(譯者注:文章鏈接已經不存在),很明顯文章描述的就是謂詞。實際上“謂詞”是邏輯編程中的名詞,模式規範旨在說明我們如何把邏輯編程中的強大功能應用到麵向對象編程中。
在域驅動設計一書中,Eric Evans詳細說明並給出了描述域模型的規範的例子。如同書中描述的一樣,Policy pattern無非就是Strategy pattern(譯者注:策略模式),也即在某種程度上,Specification pattern(譯者注:規格模式)會選擇帶有額外意圖能夠標識業務規則的不同版本的謂詞指派給域切麵。
Specification pattern建議的方法名為:isSatisfiedBy(T): boolean,該方法強調關注域的約束。正如我們前麵看到的謂詞,於 Interpreter pattern中類似,封裝在Specification對象中的原子業務邏輯可以通過布爾邏輯重組(or, and, not, any, all)。
這本書還描述了如數據庫查詢優化、歸類等高級技術。
查詢優化
接下來的內容是一些優化的技巧,我並不確定你是否會在工作中用到。確實,謂詞在過濾數據集的時候顯得有點笨拙:謂詞必須為集合中每個元素都做evaluate操作,當集合非常龐大的情況下,可能會出現性能問題。對於一個給定的謂詞,如果數據都存儲在數據庫中,通過此謂詞一個接一個地獲取龐大集合中的元素並不是一個非常好的主意。
當你遇到了性能問題,你開始分析並且找到了性能瓶頸。如果現在調用謂詞過濾數據結構中每個元素是瓶頸的話,你該如何修複這個問題呢?
刪除所有謂詞,把代碼還原成硬編碼的、更容易出錯、重複率高以及測試難度較高的形式是一種優化手段。但隻要我能找到更好的可選方案(總是能找到許多),我會拒絕此項優化。
首先,仔細觀察代碼是如何使用的。根據域驅動設計的思想,當出現問題時,應當係統審查域對象。
通常在係統中,總是會使用一些清晰的模式。經統計,這些模式為優化提供了許多可能。比如我們的PurchaseOrders 類,在我們假設的例子中,由於業務上的關係,獲取等待處理的訂單將會比其他情況多上許多。
Friend Complicity
基於不同的使用模式,你可能會編寫可選實現類進行某些特定的優化。在我們的例子中,用戶的待處理訂單經常會被查詢,我們為此編寫一個可選實現類FastPurchaseOrder,該類利用一些預先計算好的數據結構提高查詢待處理訂單的速度。
現在,為了從這個可選優化實現中受益,你也許會修改接口,增加一個專用方法,比如selectPendingOrders()。在此之前,接口中隻有一個泛型方法,添加額外方法在某些方麵是正確的,但同樣會引入一些問題:你必須在其他實現類中也實現這些方法,雖然方法隻適用於部分場合而稍顯不合理。
為了讓原先隻接受一個謂詞參數的方法在內部自行優化的技巧之一,便是讓方法與謂詞耦合起來。我借鑒了C++中friend關鍵字,把這種方式成為“Friend Complicity”。
01 |
/** Optimization method: pre-computed list of pending orders */ |
02 |
03 |
private Iterable<PurchaseOrder> selectPendingOrders() {
|
04 |
05 |
// ... optimized stuff...
|
06 |
07 |
} |
08 |
09 |
public Iterable<PurchaseOrder> selectOrders(Predicate<? super PurchaseOrder> condition) {
|
10 |
11 |
// internal complicity here: recognize friend class to enable optimization
|
12 |
13 |
if (condition instanceof PendingOrderPredicate) {
|
14 |
15 |
return selectPendingOrders(); // faster way
|
16 |
17 |
}
|
18 |
19 |
// otherwise, back to the usual case
|
20 |
21 |
return Iterables.filter(orders, condition);
|
22 |
23 |
} |
很明顯,這種方式提高了不同類之間的耦合度,這些類理應互不依賴。同時,隻能在直接提供“friend”謂詞的情況下提高性能,不能帶有裝飾或者組合模式。
Friend Complicity最重要的一點是,確保這些特定方法不做任何妥協,需要在任何時候都遵循接口的規範(即使在不需要性能提升的情況下)。同時,請記住,將來或許有一天你可能會將實現變更會未優化的實現版本。
SQL-compromised
如果訂單存儲在數據庫中,可以通過SQL快速查詢。順便提一下,你可能已經注意到謂詞正是你的SQL中where子句後的查詢條件。
一個簡單的依舊使用謂詞提升性能的方式是,實現額外的接口SqlAware,該接口帶有一個SQLasSQL()方法,方法返回謂詞實際執行時操作數據庫的SQL。當對數據庫使用這類謂詞時,謂詞會直接使用SQL查詢並返回結果,不會執行常規evaluate或者apply 方法。
我把這種謂詞入侵數據庫底層細節的方法稱為SQL妥協,通常情況下謂詞不應該這麼做。
歸類
歸類是一種描述了概念間包含關係的邏輯概念。比如,紅,綠,黃,包含於術語“色彩”中。謂詞間的歸類可以成為代碼中非常強大的實現工具。
我們來舉一個廣播股票價格的應用程序的例子。在注冊的時候,我們必須聲明對哪類股票的觀察比較感興趣。我們隻需要簡單傳遞當操作到我們感興趣的股票時返回true的股票類謂詞:
01 |
public final class StockPredicate implements Predicate<String> {
|
02 |
03 |
private final Set<String> tickers;
|
04 |
05 |
// Constructors omitted for clarity
|
06 |
07 |
public boolean apply(String ticker) {
|
08 |
09 |
return tickers.contains(ticker);
|
10 |
11 |
}
|
12 |
13 |
} |
現在我們假設應用程序已經可以利用消息主題廣播標準的股票代碼集合,每一個主題都擁有相應的謂詞。如果檢測到我們將要使用的謂詞,是被包含於或者被歸類為某個標準謂詞,我們就可以訂閱它。在我們的例子中,歸類是一個非常簡單的工作,隻需要在我們的謂詞中添加額外的方法即可:
1 |
public boolean encompasses(StockPredicate predicate) {
|
2 |
3 |
return tickers.containsAll(predicate.tickers);
|
4 |
5 |
} |
歸類操作主要關注謂詞間的包含關係。就如例子中一樣,基於集合設計的謂詞非常容易實現歸類操作,基於內部數字和日期實現的謂詞也同樣如此。否則,你可能需要求助於“Friend Complicity”,需要了解其他謂詞以便判定謂詞間的包含關係。
總之,在通常情況下,歸類是難以實現的。但即便隻是實現了部分歸類,也能帶來巨大的價值。所以說歸類是一個很重要的工具。
總結
謂詞很有趣,能夠提升代碼質量,改進思維模式。
源代碼路徑: cyriux_predicates_part2.zip (fixed broken link)
參考文獻
A touch of functional style in plain Java with predicates – Part 2 from our JCG partner Cyrille Martraire at the Cyrille Martraire’s blog blog.
最後更新:2017-05-23 10:02:26