我們不需要豐富的知識和技能,都可以執行一個程序。── 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
就有機會需要更改和重新編譯。
一般的程序會像上圖一樣,從 main
函式開始調用一些高層函式,然後是中層和低層函式。
而依賴反向,簡單來說就是反轉依賴的方向。
在研究如何實踐這個原則之前,讓我們先看看為什麼要套用這個原則吧!
為什麼要套用 DIP?
舉個例子,有一個 App,它會從 SQL 數據庫存取數據,並將結果輸出到 printer。
處理程序 (handler) 就會是查詢數據、和調用 printer 函式來輸出結果。
這個程序看起來沒有什麼問題,但如果我們想:
- 從 SQL 數據庫更改為 NoSQL 數據庫?
- 想把結果以 WhatsApp 訊息輸出,而不是 print 出來?
我們就需要編輯處理程序中調用的函式,並改變部分邏輯,以確保數據可以兼容。
而如果我們有很多調用這類函式的處理程序,我們想要添加東西的話,就會需要修改所有函式。一個簡單的要求,在程式碼庫中卻引起了巨大的變化。
這時,DIP 就大派用場了!
處理程序只需要知道從某處存取數據,及輸出結果到某處,它不需要知道其他細節。
細節並不重要。
高層函式不應依賴低層函式,兩者都應依賴於抽象介面 (abstraction)。
DIP 是什麼?
如前文所述,DIP 的意思就是反轉依賴的方向。要反轉依賴的方向,我們可以在調用者 (caller) 和被調用者 (callee) 之間添加一個穩定的抽象介面 (stable abstract interface)。
處理程序不會直接調用數據庫或 printer,而是會調用一個介面。這個介面是一個 policy,用來定義方法簽章 (method signature),也就是其引數 (argument) 和輸出。
處理程序在不知道細節的情況下調用介面,它只會關心輸入和輸出,而低層函式就負責處理細節和實作介面。
請注意,整個反向不單單是依賴關係的反向,而是介面所有權 (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 編輯小姐。