問題是否具有挑戰性,取決於你如何去看待它。
引子
很多程序員在能夠勝任一些復雜業務邏輯的開發之后,就不知道如何繼續提升自己的技術水平了。其實,這時候就需要向抽象設計之路前進啦。
何為抽象設計?抽象設計的基本功,即是將業務中的共性抽離出來,用技術語言來描述,發展技術的手段去處理。這樣,業務問題實際上就是技術問題的領域化描述,是技術問題加了一層業務的殼而已。
當能夠看清楚業務中的技術本質時,業務問題就可以轉換成原滋原味的技術問題,就再也沒必要抱怨整天都是在做業務邏輯了。
舉例子:
一個容器行為白名單業務。有兩個事件數據源,一個事件數據源上報容器行為數據,另一個事件數據源行為上報資產相關數據。這兩個事件數據源的上報是相互獨立的,但這個業務需要把容器行為數據和資產數據結合起來展示。這就存在兩個問題:1. 當容器行為數據上報時,對應的資產數據有可能還沒有上報上來,怎么解決? 2. 資產數據是會變化的,當資產數據變化時,如何更新對應的容器行為模型?
看上去,是不是一個典型的業務問題?如果把它抽象成技術語言,怎么描述? 別急,下文將會講到。
抽象設計思維能力是程序員應當具備的一項非常重要的思維能力,與邏輯能力是同等重要的。遺憾的是,很多程序員甚至終其一生都沒意識到這一點。
本文將會探討,如何訓練抽象設計的基本功:抽離共性。
抽離共性
要想抽離共性,首先要識別共性。
識別共性有一個非常簡單的方法:重復中必有共性。正是因為有共性,才會有重復。
程序員對重復代碼想必是早有耳聞,深有惡覺,—— 當然,也可能早就麻木了。誰曾想,這重復中竟然孕育着抽象設計的種子呢!誰曾想,這重復中竟然孕育着技術能力進階的機會呢!
舉例一:遍歷一個對象列表,取對象的 A 字段,得到一個列表; 遍歷一個對象列表,取對象的 B 字段,得到一個列表。 這是不是就包含重復了? 重復模式: 遍歷一個對象列表,取對象的 F 字段(F 字段可配置), 得到一個列表。 差異就是取的字段不同。大多數程序員都知道,把字段 F 的名字作為參數傳入函數即可解決。
@AllArgsConstructor
@Data
class Person {
private String name;
private Integer age;
}
public static List getValues(List<Person> persons, String fieldName) {
List values = new ArrayList();
for (Person p: persons) {
values.add(ReflectionUtil.getValue(p, fieldName)); // 這里用到了不太優雅的反射
}
return values;
}
List<Person> persons = Arrays.asList(new Person("qin", 32), new Person("ni", 24));
System.out.println(getValues(persons, "name"));
System.out.println(getValues(persons, "age"));
也可以這樣解決(把函數作為參數傳入):
public static <T> List<T> getValues(List<Person> persons, Function<Person, T> fieldFunc) {
return persons.stream().map(fieldFunc).collect(Collectors.toList());
}
System.out.println(getValues(persons, Person::getName));
System.out.println(getValues(persons, Person::getAge));
這都是雕蟲小技了。
舉例二: 遍歷一個對象列表,取對象的 A 字段,若滿足 f(A) ,則移除,得到被移除對象之后的列表; 遍歷一個對象列表,取對象的 B 字段,若滿足 g(B), 則移除,得到被移除對象之后的列表。 重復模式: 遍歷一個對象列表,取對象的 F 字段,滿足 func(F) , 則移除,得到被移除對象的列表。差異是,所取的字段 F 不同,滿足的函數 func 也不同。 這時候,很多程序員可能就不知道抽離共性了,然后就會寫兩段相似的重復代碼。這里,很多程序員能夠識別共性,但苦於缺乏相應的編程技巧,而難以抽離共性。
實際上,凡模板流程里只有少量差異的地方,都可以采用函數式編程來解決。函數式編程是抽離共性的有力的編程武器。應用函數式編程的方法是:第一步,用函數和額外參數描述差異; 第二步,在共性流程里用額外參數調用該函數。
就這個例子來說,問題就是如何描述 滿足 func(F) 的對象。可以用 Function<Person, T> fieldFunc
表達如何取字段 F 的值(類型是 T), 用 Predicate<T> test
表達對字段 F 的值進行測試。 如下所示:
public static <T> List<Person> remove(List<Person> persons, Function<Person, T> fieldFunc, Predicate<T> test) {
Iterator<Person> personIterator = persons.iterator();
while (personIterator.hasNext()) {
T t = fieldFunc.apply(personIterator.next());
if (test.test(t)) {
personIterator.remove();
}
}
return persons;
}
public static void test3() {
List<Person> persons = Lists.newArrayList(new Person("qin", 32), new Person("ni", 24));
System.out.println(remove(persons, Person::getAge, age -> age < 30));
System.out.println(remove(persons, Person::getName, name -> "qin".equals(name)));
}
如果不限定某個字段的話,也可以寫成:
public static List<Person> remove(List<Person> persons, Predicate<Person> test) {
Iterator<Person> personIterator = persons.iterator();
while (personIterator.hasNext()) {
Person p = personIterator.next();
if (test.test(p)) {
personIterator.remove();
}
}
return persons;
}
public static void test4() {
List<Person> persons = Lists.newArrayList(new Person("qin", 32), new Person("ni", 24));
System.out.println(remove(persons, p -> p.getAge() < 30));
System.out.println(remove(persons, p -> "qin".equals(p.getName())));
}
甚至,不限定具體對象類型,還可以寫成更通用的形式:
public static <T> List<T> remove(List<T> objList, Predicate<T> test) {
Iterator<T> personIterator = objList.iterator();
while (personIterator.hasNext()) {
T t = personIterator.next();
if (test.test(t)) {
personIterator.remove();
}
}
return objList;
}
public static void test5() {
List<Person> persons = Lists.newArrayList(new Person("qin", 32), new Person("ni", 24));
System.out.println(remove(persons, p -> p.getAge() < 30));
System.out.println(remove(persons, p -> "qin".equals(p.getName())));
}
當程序員的思維和編程技藝不再限定在具體行為和類型上,他就開始“飛翔”了。他在編程的世界里自由了。
可見,函數式編程,是思考和實現抽象設計能力的有力的法寶,不可不重視之。
舉例三: CRUD 里的分頁排序列表。 凡某種信息管理系統,必定有若干個分頁列表,需要根據某個字段排序。 其共性是分頁排序,不同之處在於展示、排序及用於篩選的字段不一樣。但無論客戶端分頁還是服務端分頁,其代碼幾乎是相似的。只要定義領域字段,完全可以自動生成分頁排序列表,而無需反復寫相似的模板代碼。 5 分鍾自動生成 WordPress 博客即是一例。
技能進階
隨着對共性的思考越來越多,抽離共性的技巧越來越熟練,抽離共性能力的可控范圍也越來越大,抽象設計能力也在循序漸進地提升中。
舉例四: 后台任務。比如導出任務,接收請求,存儲導出任務信息,提交任務到線程池異步執行;比如病毒掃描任務,接收請求,存儲掃描任務信息,提交任務到線程池異步執行。共性是,接收請求、存儲任務信息,提交任務到線程池異步執行。所不同的是,任務信息及特征不同。
針對這種模板流程,往往可以采用模板方法模式來抽離共性。
舉例五:通用導出。比如訂單導出,根據某些條件查詢訂單列表,從各種數據源拉取訂單相關的詳細信息(訂單信息、商品信息、支付信息、優惠信息、發貨信息、退款信息、核銷信息等),按照訂單或商品維度組裝數據,排序、過濾、匯總、格式化、生成訂單商品報表、上傳、更新導出任務信息;比如發貨導出,根據某些條件查詢發貨單號列表,從各種數據源拉取發貨相關的詳細信息(發貨單號、對應訂單商品信息、對應物流信息、對應配送信息等),按照貨單維度組裝數據,排序、過濾、匯總、格式化、生成發貨報表、上傳、更新導出任務信息;比如退款導出,根據某些條件查詢退款單號列表,從各種數據源拉取退款相關的詳細信息(退款單號、退款的訂單商品信息、退款的商品發貨狀態等)、按照退款單維度組裝數據,排序、過濾、匯總、格式化、生成退款單報表、上傳、更新導出任務信息。你能從這些導出中抽離出導出的共性嗎?
很顯然,僅僅模板方法模式還不夠。還需要更高的設計技能。比如插件式架構設計。無論什么導出,一定包含如下流程:查詢、詳情獲取、組裝數據、排序、過濾、匯總、格式化、生成報表、上傳、更新導出任務信息。無論什么查詢,通常是從 DB 或 ES 或某個數據源進行分頁批量篩選數據;無論詳情多么復雜,總是從 API 或 HBase 或某個數據源來並發批量拉取數據;無論是什么維度,總是按照某個主維度,通過一對一或一對多的方式來拼接數據;排序必定是針對某個字段;過濾總是針對某些字段的函數布爾計算;格式化可以用字段函數計算來表達;生成報表、上傳和更新導出任務信息則是典型的模板方法模式。
比較難的地方是查詢、詳情、組裝數據、排序、過濾。可以把這些操作做成插件,插件可以從不同的數據源獲取數據,指定不同的字段配置或函數計算;然后用一個通用流程把這些插件串聯起來,這樣,整個導出的共性就出來了。
除了設計模式,還需要熟悉和應用適宜的架構模式。架構模式可用來掌控更大范圍的共性。
舉例六: 事件處理流程。 資產上報事件處理流程是, 接受客戶端上報的數據,然后有一系列監聽器監聽上報的數據,做相應的處理,然后入庫;這些監聽器之間的處理可能有一定的順序依賴。入侵事件處理的通知流程是,接受已經過處理的業務事件,發送站內通知、短信,更新 Dashboard,可選地更新威脅情報。 這里的共性是什么?
組件編排。每個資產事件監聽器都是一個組件,通過某種規則串聯在一起,共同完成資產上報的處理流程;發送站內通知及短信、更新 Dashboard, 更新威脅情報,也都是組件,可以按照某種順序編排成一個完整的流程。組件編排框架的作用就是,根據組件及組件的編排順序,執行相應的組件並返回結果。
組件編排,也可以看成是插件式架構模式。
因此,抽離共性的重點在於,不是針對一個問題提出一個解決方案,而是針對一類問題提出一個解決機制。
問題是否具有挑戰性,取決於你如何去看待它。
抽離技術問題
抽象設計的另一個維度,則是從業務問題中抽離出技術問題。從技術角度思考問題,再把求解還原到業務域上。
回到之前的例子。一個容器行為白名單業務。有兩個事件數據源,一個事件數據源上報容器行為數據,另一個事件數據源行為上報資產相關數據。這兩個事件數據源的上報是相互獨立的,但這個業務需要把容器行為數據和資產數據結合起來展示。這就存在兩個問題:1. 當容器行為數據上報時,對應的資產數據有可能還沒有上報上來,怎么解決? 2. 資產數據是會變化的,當資產數據變化時,如何更新對應的容器行為模型?
如何抽離技術問題呢? 一個簡單的辦法是符號化。也就是數學采用的思維。
比如“有兩個事件數據源,一個事件數據源上報容器行為數據,另一個事件數據源行為上報資產相關數據”。可以符號化為 ES1,ES2, ES1 上報的字段有 a(A1, A2, A3, A4), ES2 上報的字段有 b(B1, B2, B3),關聯關系是 A3 與 B3 可以關聯起來。 業務需要展示出 m(A1, A2, A3, A4, B1, B2, B3)。問題1: 當 a(A1, A2, A3, A4) 上報時,如果對應的 B3 沒有上報怎么解決;問題2:當 B3 更新為 B3' 時或者被刪除時,原來的 m(A1, A2, A3, A4, B1, B2, B3) 如何變化?
這樣,是不是就完全跟業務無關了?ES1, ES2 可以代表任何業務數據源,而字段也可以是任何業務數據源的業務字段。一旦我們能夠針對這個問題建立一個解決機制,那么這個解決機制將能解決所有符合這種特征的業務問題,而不僅僅限於這一個。這種特征的業務問題可以統稱為:存在依賴變更的數據源的更新問題。而這種依賴更新問題的具體技術方案選型則需要考慮具體的業務訴求,比如實時性、依賴數據變化頻度、數據量大小、數據依賴是本質依賴還是關聯依賴、依賴是否需要解耦、操作的准確性要求等。
這就是抽象設計思維的能力所在。它能從表面的業務問題,看到深層的技術問題;通過技術問題的求解,來穿透解決一類的業務問題。
當然,要鍛煉強大的抽象設計思維,就需要多多思考共性、從業務中抽離技術問題、思考問題的本質等。這可是燒腦細胞掉頭發的事情。
小結
本文主要探討了如何抽離業務中的共性問題以及運用函數式編程技巧來實現共性的抽離。抽離共性的重點在於,不是針對一個問題提出一個解決方案,而是針對一類問題提出一個解決機制。此外,也探討了如何從業務問題中抽離技術問題的方法。
培養良好的抽象設計思維,對於程序員技術能力的進階是非常重要的。它能從表面的業務問題,看到深層的技術問題;通過技術問題的求解,來穿透解決一類的業務問題。
問題是否具有挑戰性,取決於你如何去看待它。