依賴倒置原則 (Dependency-Inversion Principle, DIP)
又稱為:相依性反向、依賴反轉原則,是物件導向系統程式中,
五個基礎設計原則 『 S.O.L.I.D 』中的 “D ” (DIP),是一種 特定的解耦 形式。
是為了:
『 解除 高階模組 (Caller 呼叫者) 與 低階模組 (Callee 被呼叫者)的 耦合關係,使高階模組不再直接依賴低階模組。 』
聽起來有點鏘… 但其實超好理解 u
目錄 [隱藏]
依賴 (Dependency)
在開始之前,得先知道什麼是『 依賴 (dependency) 』:
依賴,白話就是『需要』,但為何需要呢?
—— 達到目的、功能
即:
X 需要 Y —— > 用來達到 目的 Z(FuckU 萬年醬油小智)
例如:
- 小明需要車車 ——> 脫魯
- 我需要食物 ——> 填飽肚子
地方的媽媽需要 ——> (誤)
以程式為例:
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 介面
=> 高階模組不應該依賴於低階模組,兩者都該依賴抽象
=> 抽象不應該依賴於具體實作方式
=> 具體實作方式則應該依賴抽象
到底哪裡倒置 (反轉) 了?
原本:高階 –> 低階 (高階依賴低階)
不是應該變成 高階 <– 低階 (低階依賴高階) 才叫『反』嗎?
為什麼是變成 兩者都依賴抽象 呢?
上述範例中:
為什麼是變成 兩者都依賴抽象 呢?
上述範例中:
原本:人 (高階) 依賴 漢堡、義大利麵.. (低階)
也就是人的行為模式被漢堡、義大利麵綁死了,
漢堡變得比人類大尾,人沒有漢堡,人生就失去了希望。
就像是 8+9 與毒品的關係:
8+9 以為掌控著一切,吸食毒品只是滿足爽感,仍可控制自己的行為模式?
錯!8+9 這時已經墮落,並被毒品控制著行為模式。
8+9 以為掌控著一切,吸食毒品只是滿足爽感,仍可控制自己的行為模式?
錯!8+9 這時已經墮落,並被毒品控制著行為模式。
如果變成 人 (高階模組) 依賴 填飽肚子的東東 (抽象介面) 呢?
你提出了這個『需求』(功能):
有需求就有市場,
沒有需求就沒有買賣,沒有買賣,就沒有殺害。
這下狂了,世界萬物都繞著你轉了,
就算目前沒有具體實作 (食物),也會有 工廠 想著幫你做出來,
於是漢堡、義大利麵、烙賽大冰奶…等等產品,便隨之出現 (而非打從一開始就有這些東西)。
也就是 食物 依賴 人 『填飽肚子』 的這個需求,所以才會不斷有新的食物 (實作 Stuffer 介面) 出現,
食物依賴需求,需求是人的, ☞ 食物間接的依賴了人。
也就是 食物 依賴 人 『填飽肚子』 的這個需求,所以才會不斷有新的食物 (實作 Stuffer 介面) 出現,
食物依賴需求,需求是人的, ☞ 食物間接的依賴了人。
即:
低階模組依賴抽象,就 間接 的 依賴 了高階模組,原本 高到低 變成 低到高 的依賴關係,就是 倒置 的精神。
如果現在有個需求 (抽象),並且有一個具體實作 (低階),
但是沒有需求者 (高階),不就不存在依賴倒置嗎?
對ㄚ,阿沒有需求了,那個具體實作是要欉尛?
擴充
幾乎所有的設計原則或設計模式,都是在談『 改變 』,
讓程式擁有擴充及維護的彈性,成為最重要的課題。
由於 People 依賴 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();
也就是高階模組,仍然 依賴著 具體實作:
具體的實作 應是可『 替換 』的:
由 runtime (運行時) 傳入,而非 compile (編譯時) 就決定,所以又被稱為 —— Plugin (插件)。
解決辦法有很多,其實皆是設法,把所需得插件 (低階具體實作元件),提供給高階元件。
留言
張貼留言