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