本文為原創博文,轉載請注明出處,侵權必究!
- 概述
最近在弄阿里雲的sls日志服務,該服務提供了一個搜索接口,可根據各種運算、邏輯等表達式搜出想要的內容。具體語法可見https://help.aliyun.com/document_detail/29060.html?spm=5176.doc29029.2.2.8PG8RA。
在開發中,我們需要用到該接口的查詢需求會不斷擴增,可能一開始只用and,后面加了or和not,再后來又多了key/value pair、數值比較等等。如果把這些處理邏輯放在業務邏輯中,未免太過暴力,而且非常不方便維護和閱讀。尤其是當出現了復雜的復合邏輯時,比如:"a and b or (c and (d not e))",我們要先自己推算出具體的公式並顯示的寫在業務邏輯,顯然這是很不合理的。所以,我要把這類處理邏輯單獨抽離出來,查詢條件當成一個個搜索的過濾條件Filter,再通過拼接類Assembler自動拼接成我們想要的邏輯表達式。
- 設計單個過濾器
需要首先想清楚的是,整體的表達式是由一個個單獨的查詢語句(運算表達式)組成的,而連接他們的是邏輯運算(與或非)。所以我的思路是,先將所有單獨的運算表達式創建出來,最后通過邏輯運算將他們拼接在一起。
下面是運算表達式過濾器的實現代碼:
1 public class AliyunLogFilter { 2 3 public AliyunLogFilter() {} 4 5 public AliyunLogFilter(MatchType type, String param, int value) { //實現比較運算的表達式:type為運算符、param為查詢字段、value為該字段對應的值 6 7 singleQuery = param + " " + type.getSymbol() + " " + value; 8 } 9 10 public AliyunLogFilter(boolean isFuzzy, String param) { //實現模糊查詢的表達式:當isFuzzy為true,表示模糊查詢。 11 12 singleQuery = param + (isFuzzy ? "*" : ""); 13 } 14 15 public AliyunLogFilter(String key, String value) { //實現鍵值對查詢的表達式 16 17 singleQuery = key + ":" + value; 18 } 19 20 /** 屬性比較類型. */ 21 public enum MatchType { //屬性的比較類型,這里通過讓enum維護一個字段symbol,可以在調用時根據MatchType的類型,直接獲取對應的符號字符串。類似於多態。 22 23 EQ("="), LT(">"), ST("<"), LE(">="), SE("<="); 24 25 private String symbol; 26 27 private MatchType(String symbol) { 28 29 this.symbol = symbol; 30 } 31 32 public String getSymbol() { 33 34 return symbol; 35 } 36 } 37 38 private String singleQuery; //運算過濾器維護的唯一屬性,即單個查詢語句字符串。 39 40 public String get() { //通過get方法可獲取這個過濾器下的查詢語句。 41 42 return this.singleQuery; 43 } 44 }
單個的查詢做好了,通過構造函數我們可以直接生成對應的filter,調用get()就可以拿到他的表達式。下面只需要設計一個拼接器把多個單獨的查詢拼接在一起就好了。
- 設計過濾器拼接器
那么怎樣去設計呢?首先我想到了他的使用場景,對於單個filter,使用很簡單,每次都new Filter(param...)就可以了。但作為一個拼接工具,他的核心價值是把多個filter拼接起來的動作,而不是拼接類本身。按照傳統的方式,可能我們會這樣:在Assembler內部維護一個List<Filter>,然后維護一個List<LogicSymbol>(或者可能直接搞一個HashMap<Filter, LogicSymbol>)。然后先創建Assembler實例,把filter依次添加,像這樣:
1 Assembler assembler = new Assembler(); 2 assembler.getFilters().add(filter1); 3 assembler.getFilters().add(logicalSymbol1); 4 assembler.getFilters().add(filter2); 5 assembler.getFilters().add(logicalSymbol2); 6 assembler.getFilters().add(filter3); 7 assembler.getFilters().add(logicalSymbol3); 8 String queryStr = assembler.generateTotalQuery();
這樣寫一個明顯的缺陷就是,代碼非常的臃腫古板,而且很難明顯的看出各個filter之間的關聯,我甚至覺得generateTotalQuery里面的實現會更加復雜,因為他要對兩個list不斷的匹配重組。
於是,我想到了java 8里非常好用的stream,對於遍歷操作,stream流的鏈式寫法帶給我們極大的代碼簡潔度和可讀性。在這里,我可以同樣用鏈式寫法用一句話生成最終拼接好的查詢語句。
下面是過濾器拼接器的實現代碼:
1 public class AliyunLogFilterAssembler { 2 3 private String queryStr; //最終多個filter拼接好的完整查詢表達式。 4 5 public AliyunLogFilterAssembler() {} 6 7 public AliyunLogFilterAssembler(String queryStr) { this.queryStr = queryStr; } //重寫構造函數,可初始化查詢表達式。 8 9 public String get() { //類似於上面的單個過濾器,這里也通過get()直接拿到拼接器拼接好的表達式。 10 11 return this.queryStr; 12 } 13 14 //如果first為非操作,這里可能無法表達,暫時可直接用String參數直接傳入"not param"; 15 public static AliyunLogFilterAssembler create(AliyunLogFilter first) { //類似一個靜態的工廠方法,創建一個Assembler實例,並傳入了這個拼接器的第一個過濾條件first。 16 17 return create(first.get()); 18 } 19 20 public static AliyunLogFilterAssembler create(String firstQueryStr) { //同上,這里通過調用帶參數的構造函數,初始化了拼接器的變量queryStr。 21 22 return new AliyunLogFilterAssembler(firstQueryStr); 23 } 24 25 public AliyunLogFilterAssembler and(AliyunLogFilter filter) { //定義了"與"操作,可傳入一個過濾器,與當前的拼接器邏輯表達式形成與的關系。 26 27 return and(filter.get()); 28 } 29 30 public AliyunLogFilterAssembler and(String queryString) { //"與"操作的具體實現,遵循阿里雲提供的邏輯表達式規范,將filter的表達式拼接到拼接器中。 31 32 this.queryStr += " and (" + queryString + ")"; 33 return this; 34 } 35 36 public AliyunLogFilterAssembler or(AliyunLogFilter filter) { //同上類似,這里是"或"操作。 37 38 return or(filter.get()); 39 } 40 41 public AliyunLogFilterAssembler or(String queryString) { //同上。 42 43 this.queryStr += " or (" + queryString + ")"; 44 return this; 45 } 46 47 public AliyunLogFilterAssembler not(AliyunLogFilter filter) { //同上 48 49 return not(filter.get()); 50 } 51 52 public AliyunLogFilterAssembler not(String queryString) { //同上 53 54 this.queryStr += " not (" + queryString + ")"; 55 return this; 56 } 57 }
具體的代碼含義相信看了注釋可以理解。我把每個邏輯函數都返回了當前的assembler,這樣確保了鏈式寫法的方式,也讓assembler中的queryStr可以持續更新直到我輸入所有過濾條件。
- 驗證效果
至此,這個小輪子就算OK了,下面我們舉幾個例子來測試一下效果,對於單獨的過濾器為了簡潔,統一使用非模糊的字符串查詢。先定義幾個表達式:(1) a and b or c; (2) a and b or (c not d)
測試代碼如下:
1 String query; 2 AliyunLogFilter a= new AliyunLogFilter(false, "a"); 3 AliyunLogFilter b= new AliyunLogFilter(false, "b"); 4 AliyunLogFilter c= new AliyunLogFilter(false, "c"); 5 AliyunLogFilter d= new AliyunLogFilter(false, "d"); 6 query = AliyunLogFilterAssembler.create(a).and(b).or(c).get(); //(1) 7 System.out.println(query); 8 query = AliyunLogFilterAssembler.create(a).and(b).or(AliyunLogFilterAssembler.create(c).not(d).get()).get(); //(2) 9 System.out.println(query);
運行結果如下:
a and (b) or (c)
a and (b) or (c not (d))
雖然多了幾個括號,但表達式本身與我們所需要的邏輯是相同的含義。我把整個查詢語句的拼裝過程壓縮在了一行代碼里(上述第6、8行),大量簡化了代碼量,而且很容易寫測試代碼,也增加了可讀性和可維護性。
