我們不需要豐富的知識和技能,都可以執行一個程序。── Robert C. Martin
每一個軟件系統都會提供兩個數值:behaviour 和 structure。
開發者有責任確保軟件不但可用,而且乾淨、可讀、也易於更改。
這就是 SOLID 原則派上用場的時候!這些原則可以說是指路明燈,在任何情況下,都可以讓開發者創建更好的設計,並避免程式碼發臭。
- S:單一職責原則 (Single Responsibility Principle)
- O:開放封閉原則 (Open-Closed Principle)
- L:里氏替換原則 (Liskov Substitution Principle)
- I:介面隔離原則 (Interface Segregation Principle)
- D:依賴反向原則 (Dependency Inversion Principle)
在這篇文章中,我們會看看其中一個最重要、最常用的原則:依賴反向原則 (DIP)。
依賴是什麼?
在深入了解 DIP 之前,我們需要先知道依賴是什麼。
def funcA():
funcB()
簡單來說,如果 function A
調用 function B
,function A
就會依賴 function B
。
每當 function B
變化的時候,function A
就有機會需要更改和重新編譯。
![typical-program-1](https://appcoda.com.tw/wp-content/uploads/2022/04/Dependency-Inversion-Principle-1.png)
一般的程序會像上圖一樣,從 main
函式開始調用一些高層函式,然後是中層和低層函式。
![依賴反向原則-1](https://appcoda.com.tw/wp-content/uploads/2022/04/Dependency-Inversion-Principle-2.png)
而依賴反向,簡單來說就是反轉依賴的方向。
在研究如何實踐這個原則之前,讓我們先看看為什麼要套用這個原則吧!
為什麼要套用 DIP?
舉個例子,有一個 App,它會從 SQL 數據庫存取數據,並將結果輸出到 printer。
![handler-input-and-output](https://appcoda.com.tw/wp-content/uploads/2022/04/Dependency-Inversion-Principle-3.png)
處理程序 (handler) 就會是查詢數據、和調用 printer 函式來輸出結果。
這個程序看起來沒有什麼問題,但如果我們想:
- 從 SQL 數據庫更改為 NoSQL 數據庫?
- 想把結果以 WhatsApp 訊息輸出,而不是 print 出來?
我們就需要編輯處理程序中調用的函式,並改變部分邏輯,以確保數據可以兼容。
而如果我們有很多調用這類函式的處理程序,我們想要添加東西的話,就會需要修改所有函式。一個簡單的要求,在程式碼庫中卻引起了巨大的變化。
這時,DIP 就大派用場了!
處理程序只需要知道從某處存取數據,及輸出結果到某處,它不需要知道其他細節。
細節並不重要。
高層函式不應依賴低層函式,兩者都應依賴於抽象介面 (abstraction)。
DIP 是什麼?
如前文所述,DIP 的意思就是反轉依賴的方向。要反轉依賴的方向,我們可以在調用者 (caller) 和被調用者 (callee) 之間添加一個穩定的抽象介面 (stable abstract interface)。
![依賴反向原則-2](https://appcoda.com.tw/wp-content/uploads/2022/04/Dependency-Inversion-Principle-4.png)
處理程序不會直接調用數據庫或 printer,而是會調用一個介面。這個介面是一個 policy,用來定義方法簽章 (method signature),也就是其引數 (argument) 和輸出。
處理程序在不知道細節的情況下調用介面,它只會關心輸入和輸出,而低層函式就負責處理細節和實作介面。
![依賴反向原則-3](https://appcoda.com.tw/wp-content/uploads/2022/04/Dependency-Inversion-Principle-5.png)
請注意,整個反向不單單是依賴關係的反向,而是介面所有權 (interface ownership) 的反向。現在,定義介面的是高層函式。
而高層函式如何定義介面,就會影響低層函式的實作。由此可見,依賴關係就反轉了!
例子
讓我們來看看一些程式碼片段,以深入了解什麼是 DIP 吧!
class SqlDb:
def get(self):
print("Getting data from sql db")
def main():
sqlDb = SqlDb()
data = sqlDb.get()
如果沒有 DIP,我們會定義一個 SqlDb
類別,並直接在 main
中調用它。在這種情況下,main
就會依賴 SqlDb
,而任何在 SqlDb
中的改動,都可能令 main
需要更改。
from abc import ABC
# Interface
class DataInterface(ABC):
def get(self) -> list:
pass
# Implementations
class SqlDb(DataInterface):
def get(self):
print("Get data from sql db")
class NoSqlDb(DataInterface):
def get(self):
print("Get data from no sql db")
# Factory
def getDataHandler() -> DataInterface:
handler = SqlDb()
return handler
def main():
anyDataHandler = getDataHandler()
data = anyDataHandler.get()
在以上的例子中,我們定義了一個抽象類別 DataInterface
來指定 policy。也就是說,DataInterface
類別必須實作 get
函式。
SqlDb
和 NoSqlDb
都是利用 get
函式來實作 DataInterface
。
main
函式調用 getDataHandler
來獲取 DataInterface
。它不會關心 dataHandler
是什麼;只會關心 dataHandler
必須實作一個 get
函式、並回傳一組數據。
如果想轉換到另一個數據庫 firebase
,我們只需要建立一個新的 Firebase
類別並實作 get
函式,然後就可以在 getDataHandler
中換掉原本的數據庫了。如此一來,main
函式就不會被影響到。
這也通常被稱為開放封閉原則。
好處
現在,你應該也很清楚 DIP 有什麼好處吧!在文章結束之前,讓我們看看其中兩個好處吧!
細節的改變不會影響業務邏輯 (business logic)
細節是不穩定的,我們隨時都可能會更改數據庫或輸出的實作。
但介面就相對比較穩定。
在實作中的一個改動,不一定會影響到介面都要跟著更改。
如此一來,我們就可以在不影響業務邏輯的情況下,在實作中添加更多功能。
不必急於作出關於細節的決定
在 DIP 原則下,業務邏輯可以對細節一無所知。因此,我們就可以有更多時間,來實作之後的細節。
我們的時間越多,就能夠獲取更多信息,來做出正確的決定。
一位好的架構師 (Architect),應該盡量延遲作出所有決定的時間。── Robert C. Martin
有了介面,我們可以暫時利用 Local Storage,就不需要急於選擇數據庫。之後,我們就可以在不影響業務邏輯的情況下,更改實作的細節。
一位好的架構師就應該設計好 policy,讓自己可以在適當的時候,才作出有關細節的決定。
總結
以上就是依賴反向原則的詳細說明!這可說是 5 個 SOLID 原則中最重要和最基礎的概念。
希望你覺得這篇文章有用,我們在下一篇文章再見!
只有特別用心的人,才可以編寫出無瑕的程式碼。── Robert C. Martin
作者簡介: Jason Ngan,Shopee 後端工程師。
譯者簡介:Kelly Chan-AppCoda 編輯小姐。