依賴倒置,究竟什么被倒置了?
當我們需要某個類A中使用到另外一個類B時,最直接的方式就是在A中直接依賴B,但是,今天我們要講解的主角卻是反其道而行之,它就是依賴倒置原則,那么,什么是依賴倒置原則?這種反向思維可以帶來什么收益?這篇文章就來聊一聊。
什么是依賴倒置?
依賴倒置原則,英文為:Dependency inversion principle(簡稱DIP),也是 Robert C. Martin提出的 SOLID原則中的一種,老規矩,還是先看看作者 Robert C. Martin 對接口依賴倒置原則是如何定義的:
The Dependency Inversion Principle (DIP) states that high-level
modules should not depend on low-level modules; both should
depend on abstractions. Abstractions should not depend on details.
Details should depend upon abstractions.
通過作者對依賴倒置的定義,可以總結出其核心思想是:高層模塊不應該依賴低層模塊,兩者都應該依賴于抽象。抽象不應該依賴于細節,細節應該取決于抽象。
直接依賴的問題
對于上述依賴倒置的定義,如何理解呢?我們先來看下傳統這種直接依賴會存在什么問題?如下為一張直接依賴的關系圖:
在上圖中,高層組件 ObjectA直接依賴于低層組件 ObjectB,高層組件的重用機會受到限制,因為任何對低層組件的更改都會直接影響高層組件。
為了更好的說明直接依賴的問題,這里以一個真實的電商場景為例進行說明,其中有一個高層模塊 OrderService用于處理訂單,這個高層模塊依賴于一個低層模塊 OrderRepository來存儲和檢索訂單數據。示例代碼如下:
// 高層模塊:OrderService
public class OrderService {
private MySQLOrderRepository mySQLRepository;
public OrderService(MySQLRepository mySQLRepository) {
this.mySQLRepository = mySQLRepository;
}
public void createOrder(Order order) {
// 一些業務邏輯
mySQLRepository.save(order);
}
}
// 低層模塊:MySQLRepository
public class MySQLRepository {
public void save(Order order) {
// 使用 MySQL數據庫保存訂單
}
}
在上述例子中,OrderService直接依賴于 OrderRepository,這種設計存在幾個缺點:
- 緊耦合:如果要把數據庫從 MySQL切換到其他的數據庫,我們需要修改 OrderService,因為它直接依賴于 OrderRepository。
- 難以測試:在進行單元測試時,我們無法輕松地對 OrderService 進行模擬,因為它直接依賴于具體實現 MySQLRepository。
- 重用性差:如果在另一個項目中我們需要使用 OrderService 但存儲訂單的方式不同,例如使用文件系統或遠程服務,我們將無法直接重用 OrderService。
那么,對于這些缺點,該如何解決呢?接下來我們將重點講解。
如何實現依賴倒置?
這里提供兩種主流的解決方案。
方案一:引入抽象層
通過低級組件實現高級組件的接口,要求低級組件包依賴于高級組件進行編譯,從而顛倒了傳統的依賴關系,如下圖:
圖1中,高層對象A依賴于底層對象B的實現;圖2中,把高層對象A對底層對象的需求抽象為一個接口A,底層對象B實現了接口A,這就是依賴反轉。
因此,上面的問題我們也可以通過引入一個抽象層 OrderRepository來解耦高層模塊和低層模塊,整個關系圖如下:
通過這種方式,OrderService依賴于 OrderRepository接口而不是具體實現 MySQLRepository。這樣,我們可以輕松替換低層實現而無需修改高層模塊,修改后的代碼如下:
// 高層模塊:OrderService
public class OrderService {
private OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void placeOrder(Order order) {
// 一些業務邏輯
orderRepository.save(order);
}
}
// 抽象層:OrderRepository接口
public interface OrderRepository {
void save(Order order);
}
// 低層模塊:MySQLRepository實現
public class MySQLRepository implements OrderRepository {
public void save(Order order) {
// 使用MySQL數據庫保存訂單
}
}
// 另一個低層模塊:PostgreSQLRepository實現
public class PostgreSQLRepository implements OrderRepository {
public void save(Order order) {
// 使用PostgreSQL數據庫保存訂單
}
}
在應用程序中,我們可以靈活選擇使用哪種具體實現,也可以把數據庫的選擇做成配置:
OrderRepository orderRepository = new MySQLRepository(); // 或 new PostgreSQLRepository();
OrderService orderService = new OrderService(orderRepository);
通過這種方式,OrderService變得更具重用性、可測試性更強,并且與具體的存儲實現解耦,滿足依賴倒置原則的要求。
方案二:引入抽象層升級版
盡管方式一也實現了依賴倒置,但是這種實現方式高層組件以及組件是封裝在一個包中,對低層組件的重用會差一些,因此,另一種更靈活的解決方案是將抽象組件提取到一組獨立的包/庫中,如下圖:
因此,上述電商示例的依賴關系會變成下圖:
這種實現方式將每一層分離成自己的封裝,鼓勵任何層的再利用,提供穩健性和移動性。
兩種方案的核心思想都是一樣的,只是在靈活性和組件復用的考慮上略有差異。
依賴倒置的實例
在 Java語言中,使用依賴倒置原則的框架或者技術點有很多,這里列舉2個比較較常用的例子:
1.Spring
Spring框架的核心之一是依賴注入(Dependency Injection, DI),這是依賴倒置原則的一個實現。通過Spring容器管理對象的創建和依賴關系,可以使得高層模塊和低層模塊都依賴于抽象。Spring支持構造器注入、setter注入和接口注入等多種方式。
2.Java SPI
Java SPI(Service Provider Interface)機制也體現了依賴倒置原則,SPI機制通過定義接口和服務提供者(Service Providers),使得高層模塊(使用者)和低層模塊(提供者)之間的依賴關系可以通過接口進行解耦。具體來說,高層模塊依賴于抽象(接口),而不是具體的實現,從而實現了依賴倒置原則。
JDBC(Java Database Connectivity)就是使用 SPI機制來加載和注冊數據庫驅動程序,使得應用程序可以動態地使用不同的數據庫而無需修改代碼。
JDBC SPI的工作原理:
- 定義服務接口:JDBC API定義了一組接口,如 java.sql.Driver。
- 實現服務接口:每個數據庫廠商實現這些接口,例如,MySQL的驅動實現了 java.sql.Driver接口。
- 聲明服務提供者:數據庫驅動的JAR包中包含一個文件,聲明實現類。
- 加載服務提供者:通過 ServiceLoader或 JDBC API動態加載并實例化驅動實現。
總結
本文通過一個電商示例分析了什么是依賴倒置原則,并且提出了依賴倒置的兩種實現風格,通過引入抽象層,可以降低系統的耦合度,提升系統的擴展性和可維護性。因此,在實際開發中,我們應當始終遵循依賴倒置原則,設計靈活、可擴展的系統架構,從而應對復雜多變的業務需求。