當我們需要某個類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個比較較常用的例子:
Spring
Spring框架的核心之一是依賴注入(Dependency Injection, DI),這是依賴倒置原則的一個實現。透過Spring容器管理物件的建立和依賴關係,可以使得高層模組和低層模組都依賴於抽象。Spring支援構造器注入、setter注入和介面注入等多種方式。
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動態載入並例項化驅動實現。
總結
本文透過一個電商示例分析了什麼是依賴倒置原則,並且提出了依賴倒置的兩種實現風格,透過引入抽象層,可以降低系統的耦合度,提升系統的擴充套件性和可維護性。因此,在實際開發中,我們應當始終遵循依賴倒置原則,設計靈活、可擴充套件的系統架構,從而應對複雜多變的業務需求。