編程范式巡禮第三季 談談依賴反轉
今天會進入深一點的主題,談一個軟件開發的"道":依賴反轉。根據我的觀察,這也是架構師與程序員的分水嶺之一。
什么是依賴反轉
引出問題
讓我們從Uncle Bob和小明的一段對話開始。原文地址:如何成為一名優秀的架構師
小明:我要領導一個團隊,還要做所有關于數據庫、框架和Web服務器的重要決定。
Uncle Bob:好吧,如果是這樣,你就沒必要成為一名軟件架構師了。
小明:當然有必要了!我要成為一個能夠做所有重要決定的人。
Uncle Bob:這樣很好,只是你沒有列出哪些才是重要的決定。你剛才說的那些跟重要的決定沒有什么關系。
Bob大叔一上來就拋出了一個反常識的觀點,工具的選擇決策并不是重要的問題,為什么這么認為?
Uncle Bob:你認為業務邏輯依賴數據庫,但實際上不是這樣的。如果你的架構足夠好,最起碼業務邏輯不應該依賴數據庫。
小明:如果業務邏輯對數據庫一無所知,它怎么使用這些工具呢?
Uncle Bob:依賴反轉。你要讓數據庫依賴業務邏輯,而不是讓業務邏輯依賴數據庫。
Bob大叔認為重要的是業務邏輯的實現,工具只是服務于業務邏輯。但小明提出的是一個非常實際的問題,業務邏輯是基于工具來實現的,就好比畫家是依賴畫具創作的,東方和西方由于畫具不同,展現的藝術作品就完全不同。這個時候Bob大叔拋出了這次的重頭戲:依賴反轉。
小明:那就更加費解了!既然上層策略(假設你指的是業務邏輯)要調用下層策略(假設你指的是數據庫),那么就應該是上層策略依賴下層策略,就像調用者依賴被調用者一樣。這是眾所周知的!
Uncle Bob:在運行時確實是這樣的,但在編譯時我們要把依賴反轉過來。上層策略的代碼不要引用任何下層策略的代碼。
小明的說法有道理,適用于我們現實生活常識,但是計算機領域恰恰是有自己獨特規則的,Bob大叔指出了這一點。我們編寫的計算機代碼并不是直接運行的,當中會經過編譯器的處理,而這個中間處理,讓依賴反轉變成了可能。通俗點說,在計算機世界中,工具是可以晚于業務邏輯出現的。
代碼實例
下面來看代碼的例子:
小明:在Java里,發送者最起碼要知道接收者的基本類型。
Uncle Bob:是的。不過,不管是哪一種情況,發送者都不知道接收者具體的類型。
發送者(業務邏輯):BusinessRule
基本類型:BusinessRuleGateway
具體類型(工具):MySqlBusinessRuleGateway
package businessRules;
import entities.Something;
public class BusinessRule {
private BusinessRuleGateway gateway;
public BusinessRule(BusinessRuleGateway gateway) {
this.gateway = gateway; }
public void execute(String id) {
gateway.startTransaction();
Something thing = gateway.getSomething(id);
thing.makeChanges();
gateway.saveSomething(thing);
gateway.endTransaction();
}
}
import entities.Something;
public interface BusinessRuleGateway {
Something getSomething(String id);
void startTransaction();
void saveSomething(Something thing);
void endTransaction();
}
package database;
import businessRules.BusinessRuleGateway;
import entities.Something;
public class MySqlBusinessRuleGateway implements BusinessRuleGateway {
public Something getSomething(String id) {
// 從MySQL里讀取一些數據
}
public void startTransaction() {
// 開始一個事務
}
public void saveSomething(Something thing) {
// 把數據保存到MySQL
}
public void endTransaction() {
// 結束事務
}
}
可以看到,業務邏輯BusinessRule是在運行時對工具MySqlBusinessRuleGateway進行調用的,但在編譯時,兩者并沒有依賴關系。
基本類型的問題
一切都是那么的完美,依賴確實反轉了,但是存在一個問題,就是多出了一個東西:基本類型BusinessRuleGateway。
小明:這樣的話,如果業務邏輯需要所有的工具,那么你必須把所有工具都放到Gateway接口里。
小明:這樣的話,你就會有很多接口,而且有很多實現類。
小明:這樣子很浪費時間!我為什么要這樣做呢?這樣只會增加更多的代碼。
Uncle Bob:這個叫作接口分離原則。每個業務邏輯只使用一部分數據庫工具,所以每個業務邏輯只定義能夠滿足需要的接口。
小明提出了一個開發中很實際的問題,基本類型是多余的代碼,會增加工作。Bob大叔則覺得,基本類型可以認為是對工具的需求,也是需要思考的部分。
小明:但首先要先決定使用什么數據庫、Web服務器或框架啊!
Uncle Bob:不,實際上應該在開發后期才開始做這些事情——在你掌握了更多信息之后。
哀哉,當架構師草率地決定要使用一個數據庫,后來卻發現使用文件系統效率更高。
哀哉,當架構師草率地決定使用一個Web服務器,后來卻發現團隊需要的不過是一個Socket接口。
哀哉,當架構師草率地決定使用一個框架,后來卻發現框架提供的功能是團隊不需要的,反而給團隊帶來了諸多約束。
幸哉,當架構師在掌握了足夠多的信息后才決定該用什么數據庫、Web服務器或框架。幸哉,當架構師為團隊鑒別出運行緩慢、耗費資源的IO設備和框架,這樣他們就可以構建飛速運行的輕量級測試環境。
幸哉,當架構師把注意力放在那些真正重要的事情上,并把那些不重要的事情放在一邊。
Bob大叔用一段詠嘆調結束了這一次對話,提出了他的核心看法:架構設計要能適應未來的變化。
- 在技術層面,最大的挑戰來自于無法預測的性能容量增長,需要不斷與更先進的工具進行接軌。
- 在業務層面,響應要求日益嚴峻,代碼的修改成本(主要由耦合帶來)會成為重要的生產力指標。
解決思路是模塊與工具解耦、模塊與模塊解耦,依賴反轉無疑是實現解耦有力方法,是架構師的有力工具。
依賴反轉思想的擴展
依賴反轉不僅僅是一個模式或者方法,更重要的是其體現的解耦思想,下面再介紹兩個具有同樣思想的重要范式。
切面范式
切面Aspect是與程序的縱向主流執行方向橫向正交的關注焦點。此類代碼以片斷的形式散落在各處,雖具有相似的邏輯,卻無法用傳統的方法提煉成模塊。典型的例子如:日志輸出、代碼跟蹤、性能監控、異常處理、安全檢查、事務處理等。為解決此類問題,AOP應運而生。它將每類橫切關注點封裝到單獨的Aspect模塊中,通過定義執行點和代碼綁定起來。
AOP從描述來看是比較抽象的,簡單來說就是除了上面提到的模塊和工具、模塊和模塊以外,在模塊內代碼片斷之間也存在一定的依賴關系,而AOP是對代碼片斷解耦的方法。
下面是AOP代碼片斷(進行日志跟蹤)。
@Around("execution(* spring.services.MyDemoService3.*(..))")
public void traceBusiness(ProceedingJoinPoint jp) throws Throwable {
System.out.println("before enter method "+jp.getSignature().getName());
jp.proceed(jp.getArgs());
System.out.println("after enter method "+jp.getSignature().getName());
}
有人會問了,為啥沒有業務邏輯。這是因為,AOP與業務邏輯是可以無關的,業務邏輯角度有可能無法感知。但是信息不透明容易形成惡性的發展,所以實際使用時要運用annotation進行限制,避免過度使用。
泛型范式
泛型編程是算法導向的,即以算法為起點和中心點,逐漸將其所涉及的概念內涵模糊化、外延擴大化,將其所涉及的運算抽象化、一般化,從而擴展算法的適用范圍。
泛型又是解耦思想在具體算法實現中的運用。算法在運行時是包含數據結構和算法邏輯兩個部分的,這個時候我們可以用基本類型來替代具體的數據結構,實現兩者的解耦。
下面是代碼例子(將一組Json字符轉換為對象),轉換的目標對象與算法邏輯是無關的,所以用基本類型T來進行了替代。
static <T> List<T> convertJsonToPojo(List<String> jsonStrings, Class<T> c, boolean generateSeq) {
List<T> result = new ArrayList<>();
ObjectMapper objectMapper = ObjectMapperFactory.create();
jsonStrings.forEach(map -> {
result.add(convertJsonToPojo(map, c, generateSeq, objectMapper));
});
return result;
}
泛型范式與日常工作非常接近,我們每時每刻都能接觸到,是除了基本范式之外最為古老的了。編寫泛型代碼一定會用到抽象思維,這不是一種常識思維,但可以作為一種練習,是由低到高的修煉捷徑。
小結
最后想說的是,最重要的不是依賴反轉這個方法,而是依賴反轉的思想。練成了這種思想,小處講,可以節省代碼、提高效率。大處講,可以適應變化、應對未來。這里只是拋個磚,請有志于架構設計的同學務必深入掌握。