依賴反向原則 (Dependency-Inversion Principle, DIP),
[dɪˋpɛndənsɪ] [ɪnˋvɝʃən] [ˋprɪnsəp!],
又譯為:相依性反向、依賴反轉原則,是物件導向系統程式中,
五個基礎設計原則 『 S.O.L.I.D 』中的 “D ” (DIP),是一種 特定的解耦 形式。
是為了:
『 解除 高階模組 (Caller 呼叫者) 與 低階模組 (Callee 被呼叫者)的 耦合關係,
使高階模組不再直接依賴低階模組。 』
目錄
依賴 (Dependency)
在開始之前,得先知道什麼是『 依賴 (dependency) 』:
依賴,白話就是『需要』,但 為何需要呢?
—— 達到目的、功能
即:
X 需要 Y —— > 用來達到 目的 Z
例如:
- 小明需要車車 ——> 脫魯
- 我需要食物 ——> 填飽肚子
地方的媽媽需要 ——> (誤)
以程式為例:
public class Main {
public static void main(String[] args) {
// 實例 People 物件 -- 我
// 執行 空引數建構元
People me = new People();
// 開吃囉
me.eat();
}
}
class People {
private Hamburger burger;
public People() {
// 得到一個漢堡
burger = new Hamburger();
}
public void eat() {
// 填飽肚子
burger.stuff();
}
}
class Hamburger {
public void stuff() {
System.out.println("咔拉雞腿滿福堡 好棒棒");
}
}
// Result:
// 咔拉雞腿滿福堡 好棒棒
解釋:『 我 Me 』依賴『 漢堡 Hamburger 』用來『 填飽肚子 』,
當自身需要呼叫其他類別的實例時,就稱這樣的關係為 — 依賴。
如果今天不想吃漢堡了,怎辦?
So easy~ 吃麵啊:
public class Main {
public static void main(String[] args) {
// 實例 People 物件 -- 我
// 執行 空引數建構元
People me = new People();
// 開吃囉
me.eat();
}
}
class People {
private Spaghetti spaghetti;
public People() {
// 得到一碗義大利麵
spaghetti = new Spaghetti();
}
public void eat() {
// 填飽肚子
spaghetti.fill();
}
}
class Spaghetti {
public void fill() {
System.out.println("大蒜辣椒麵 :D");
}
}
class Hamburger {
public void stuff() {
System.out.println("咔拉雞腿滿福堡 好棒棒");
}
}
// Result:
// 大蒜辣椒麵 :D
聰明如你,應該看出問題所在了
依賴產生的問題:
- 我只是想填飽肚子,為什麼要 指定 依賴漢堡,難道我這輩子只能跟他在一起了嗎?
- 幸好,把程式碼改一改,就可以改吃義大利麵了,但我變成依賴於義大利麵了耶…
- 難道有一百種食物,我換個口味,就要 改一百次程式碼 嗎 😑?
- 呼叫的動詞 (方法名稱),不小心從
stuff()
變成fill()
,改來改去有點麻煩兒 ._.
依賴反向原則
(Dependency Inversion Principle, DIP)
以上的例子可以看出:
我們真正所需要的、依賴的,其實不是實際的類別與物件,而是他所擁有的功能。
其實這就是 依賴反向原則 DIP (Dependency Inversion Principle):
- 高階模組不應該依賴於低階模組,兩者都該依賴抽象。
- 抽象不應該依賴於具體實作方式。
- 具體實作方式則應該依賴抽象。
名詞解釋
高階與低階,是相對關係,其實也就是 呼叫者 (Caller) 與 被呼叫者 (Callee),
此例中 People 為高階,Hamburger、Spaghetti… 為低階模組。抽象,是指 介面 (interface) 或是 抽象類別 (Abstract Class),
也就是不知道實作方式,無法直接被實例化。具體實作方式,就是指有實作介面或是繼承抽象的 非 抽象類別。
(繼承抽象類別者,有可能還是抽象類別,如以下範例中的:AbstractB)
abstract class AbstractA {
// 抽象方法 eat()
public abstract void eat();
}
abstract class AbstractB extends AbstractA {
/*
* 可以不用實作 父類別 的 抽象方法 eat()
* 因為自己本身也為 抽象類別
*/
// 新增 抽象方法 drink()
public abstract void drink();
}
/**
* 具體實作類別
* 繼承抽象類別 AbstractB
* 且由於 AbstractB 繼承自 AbstractA
*
* 必須實作 AbstractA 的方法 eat()
*/
class ConcreteC extends AbstractB{
@Override
public void eat() {
}
@Override
public void drink() {
}
}
介面導向程式設計
提到 功能,大家馬上會聯想到 — — 介面 (interface)。
介面的中心思想是: "封裝隔離",
也就是外部類別只需要呼叫介面提供的方法,不用也不需要知道內部如何實作。
依據『介面導向程式設計』,善用介面的好處,使得系統具有高維護性與彈性,
不論是擴充或重構,外部呼叫類別僅會受到最小幅度的影響 (甚至不受影響)。
另外,選擇使用介面或抽象類別時:除非需要為子類別提供公共功能,否則 優先使用介面。
在例子中,依賴的功能是 填飽肚子
因此定義一個 介面 (interface)
interface Stuffer {
// 填飽肚子
void stuff();
}
p.s 你可能疑惑,為什麼不用抽象類別 Food (食物) ,不是比較簡單好理解嗎?
因為範例中說的需求是填飽肚子,石頭 也可以填飽肚子,但它並非食物!
修改一下程式碼:
public class Main {
public static void main(String[] args) {
// 實例 People 物件 -- 我
// 執行 空引數建構元
People me = new People();
// 開吃囉
me.eat();
}
}
class People {
private Stuffer stuffer;
public People() {
// 得到一個填充者 的實例
// 實際實作種類是 Hamburger
stuffer = new Hamburger();
}
public void eat() {
// 填飽肚子
stuffer.stuff();
}
}
interface Stuffer {
// 填飽肚子
void stuff();
}
class Hamburger implements Stuffer{
@Override
public void stuff() {
System.out.println("咔拉雞腿滿福堡 好棒棒");
}
}
// Result:
// 咔拉雞腿滿福堡 好棒棒
注意上方 填充者實例的建構方式是
Stuffer stuffer = new Hamburger();
而不是!!
Hamburger burger = new Hamburger();
你會說:靠邀!講了這麼多
不就只是把前面的類別名稱 改成父類別 (介面)
後面還不是都一樣,用 new Hamburger();
來建構物件。
沒錯 ㄏㄏ
理解此處,正是 介面導向程式設計原則 的第一步
僅僅這樣一個小動作:
People 類別 依賴的對象,變成了抽象、介面 (Stuffer
),而非實際類別 (漢堡、義大利麵、茶)
也就實現了 依賴反向原則:『 要相依於抽象,不要相依於實際類別 』
原本的依賴關係:
改變為:
依賴反轉原則的目的是為了 解除高階模組 (People) 與低階模組 (Hamburger) 的耦合關係
People 不再依賴 Hamburger ,而是兩者都依賴 Stuffer 介面
=> 高階模組不應該依賴於低階模組,兩者都該依賴抽象
=> 抽象不應該依賴於具體實作方式
=> 具體實作方式則應該依賴抽象
到底哪裡反向了?
原本:高階 –> 低階 (高階依賴低階)
不是應該變成 高階 <– 低階 (低階依賴高階) 才叫『反』嗎?
為什麼是變成 兩者都依賴抽象 呢?
上述範例中:
原本:人 (高階) 依賴 漢堡、義大利麵.. (低階)
也就是人的行為模式被漢堡、義大利麵綁死了,
漢堡變得比人類大尾,人沒有漢堡,人生就失去了希望。
若 人 (高階模組) 依賴 填飽肚子的東東 (抽象介面) 呢?
你提出了這個『需求』(功能):
有需求就有市場,
沒有需求就沒有買賣,沒有買賣,就沒有殺害。
這下狂了,世界萬物都繞著你轉了,
就算目前沒有具體實作 (食物),也會有 工廠 想著幫你做出來,
於是漢堡、義大利麵、烙賽大冰奶…等等產品,便隨之出現 (而非打從一開始就有這些東西)。
也就是 食物 依賴 人 『填飽肚子』 的這個需求,所以才會不斷有新的食物 (實作 Stuffer 介面) 出現,
食物依賴需求,需求是人的, ☞ 食物間接的依賴了人。
低階模組依賴抽象,就 間接 的 依賴 了高階模組,
原本 高到低 變成 低到高 的依賴關係,就是 反向 的精神。
如果現在有個需求 (抽象),並且有一個具體實作 (低階),
但是沒有需求者 (高階),不就不存在依賴反向嗎?
對啊,阿沒有需求了,那個具體實作是要欉尛?
擴充
幾乎所有的設計原則或設計模式,都是在談『 改變 』,
讓程式擁有擴充及維護的彈性,成為最重要的課題。
由於 People 依賴 Stuffer 介面,就算未來替換 Stuffer 實作子類別 (食物),
也不用更改任何方法呼叫邏輯,確保了 統一的呼叫方式,
也不會像範例中,出現以下的腦殘現象 (不同食物,呼叫方法就不一樣):
hamburger.stuff(); // 吃漢堡
vs
spaghetti.fill(); // 吃義大利麵
譬如,欲擴充一個新的類別 Tea,只需要實作介面,
高階模組 的 呼叫方式完全一樣:
class Tea implements Stuffer {
@Override
public void stuff() {
System.out.println("烙賽 大冰奶");
}
}
使得系統大大的增加了彈性且容易擴充!
使得系統大大的增加了彈性且容易擴充!
使得系統大大的增加了彈性且容易擴充!
— 覺得很重要 o.o
結語
騙你 der~ 僅僅這樣,依賴反向原則 “尚未”完成,只是為了讓你好理解,
因為還存在一個大問題 — People 仍必須自己去 new 一個具體實作!
Stuffer stuffer = new Tea();
也就是高階模組,仍然 依賴著 具體實作:
這違反了 介面的思想:“封裝隔離 (忘記點我)“,People 不應知道具體的實作類別是誰,
具體的實作 應是可『 替換 』的:
由 runtime (運行時) 傳入,而非 compile (編譯時) 就決定,
所以又被稱為 —— Plugin (插件)。
解決辦法有很多,其實皆是設法,把所需得插件 (低階具體實作元件),提供給高階元件。
讓依賴關係,可以變成理想的:
世界和平了
完整的依賴反向原則,以後再慢慢講嚕 >.^
範例原始檔
在《依賴反向原則 (Dependency-Inversion Principle, DIP)》中有 30 則留言
Dear
Bassist 中勝
看完你的依賴倒置原則,讓我更加了解類別們的關係
並且你那幽默風趣的敘述口吻總是讓我記憶猶新
感謝你提供這麼好的資訊給大家分享~
阿彌陀佛
續集呢!?(敲碗
另外可否解說一點UML的圖
不太懂虛線實線、黑箭頭跟白箭頭的差別。
您好:
最近比較繁忙,會趕緊找時間打續集 😂
感謝支持!
簡單說明:
因此文中的例子中,People 與 Stuffer 的關係,是可以替換為此的。 (強依賴)
且低階模組改變,會影響高階模組。 是一個很模糊且廣泛的關係,這也是為什麼 UML 會有這麼多 stereotypes 來表達 依賴。
『提問』請教~您的程式碼是怎麼呈現的!?
您好:
我使用 highlight.js 來顯示程式碼
很不賴 以加到最愛 聊OOD的都是珍貴的地方 ~
感謝您 😃
之前也有讀過相關的文章,但老是覺得沒有吸收進來;謝謝你的分享,直的很簡單易懂。希望以後可以看到更多你的文章~~
認真感謝! 😭
嗚嗚嗚 聽了覺得好感動
我會加油的 謝謝支持 😃
持續關注大大的優質好文
淺顯易懂真的讓新手受益良多
很期待你的相關文章哦:)
謝謝你 😆
目前正在進行秘密計畫
會累積一小段時間
敬請期待 😂
Good Job man!
欉尛…真虧你想得出來…
哈哈 撰文不帶髒字好難😂
這一篇好棒棒 感謝分享!
謝謝 🙂
真的寫得很棒,很用心 XD
感謝您!我繼續努力!😆
很清楚地介紹,讓我這新手收穫很多!
感謝!
感謝鄭大的介紹,非常適合初學者閱讀,但想請問最後People還是依賴Hamburger的實作解法為何? 謝謝
您好 😁,一種常見解法:
[工廠方法模式] (文章升級中,暫時鎖起來了,可能得麻煩 google 一下 😅)
也可以先參考我的:
[控制反轉 & 依賴注入] https://blog.jason.party/3/ioc-di
受益良多,期待能看到更多的文章。
已加到我的最愛。
好好笑,很好懂!
謝謝 😇
謝謝大師QQ
這篇是我一堆文章唯一看得懂的了
真的很棒