設計模式/原則

依賴倒置原則 (Dependency-Inversion Principle, DIP)

依賴倒置原則 (Dependency-Inversion Principle, DIP)
[dɪˋpɛndənsɪ] [ɪnˋvɝʃən] [ˋprɪnsəp!],
又稱為:相依性反向、依賴反轉原則,是物件導向系統程式中,
五個基礎設計原則 『 S.O.L.I.D 』中的 “D ” (DIP),是一種 特定的解耦 形式。
 
是為了:

『 解除 高階模組 (Caller 呼叫者) 與 低階模組 (Callee 被呼叫者)的 耦合關係,
使高階模組不再直接依賴低階模組。 』

 

階層(hierarchy)
 
聽起來有點鏘… 但其實超好理解唷
 

依賴 (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

 
聰明如你,應該看出問題所在了
依賴產生的問題:

  1. 我只是想填飽肚子,為什麼要 指定 依賴漢堡,難道我這輩子只能跟他在一起了嗎?
  2. 幸好,把程式碼改一改,就可以改吃義大利麵了,但我變成依賴於義大利麵了耶…
  3. 難道有一百種食物,我換個口味,就要 改一百次程式碼 嗎 😑?
  4. 呼叫的動詞 (方法名稱),不小心從 stuff() 變成 fill() ,改來改去有點麻煩兒 ._.

 


 

依賴倒置原則
(Dependency Inversion Principle, DIP)

以上的例子可以看出:

我們真正所需要的、依賴的,其實不是實際的類別與物件,而是他所擁有的功能
其實這就是 依賴倒置原則 DIP (Dependency Inversion Principle)

  1. 高階模組不應該依賴於低階模組,兩者都該依賴抽象。
  2. 抽象不應該依賴於具體實作方式。
  3. 具體實作方式則應該依賴抽象。

 
(有點兒複雜… 看不懂的話沒差,接著往下)
 
 

名詞解釋

  • 高階與低階,是相對關係,其實也就是 呼叫者 (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)
 
介面的中心思想是: "封裝隔離"
也就是外部類別只需要呼叫介面提供的方法,不用也不需要知道內部如何實作。
 
依據『介面導向程式設計』,善用介面的好處,使得系統具有高維護性與彈性,
不論是擴充或重構,外部呼叫類別僅會受到最小幅度的影響 (甚至不受影響)。
另外,選擇使用介面或抽象類別時:除非需要為子類別提供公共功能,否則 優先使用介面
 
DIP-electricity-ex
 
 
在例子中,依賴的功能是 填飽肚子
因此定義一個 介面 (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 這時已經墮落,並被毒品控制著行為模式。
 
 

人 (高階模組) 依賴 填飽肚子的東東 (抽象介面) 呢?

你提出了這個『需求』(功能):

有需求就有市場,沒有需求就沒有買賣,沒有買賣,就沒有殺害

 
這下狂了,世界萬物都繞著你轉了,
就算目前沒有具體實作 (食物),也會有 工廠 想著幫你做出來,
於是漢堡、義大利麵、烙賽大冰奶…等等產品,便隨之出現 (而非打從一開始就有這些東西)。
 
 
也就是 食物 依賴 人 『填飽肚子』 的這個需求,所以才會不斷有新的食物 (實作 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)》中有 28 則留言

  1. Dear
    Bassist 中勝
    看完你的依賴倒置原則,讓我更加了解類別們的關係
    並且你那幽默風趣的敘述口吻總是讓我記憶猶新
    感謝你提供這麼好的資訊給大家分享~

  2. 續集呢!?(敲碗
    另外可否解說一點UML的圖
    不太懂虛線實線、黑箭頭跟白箭頭的差別。

    1. 您好:
      最近比較繁忙,會趕緊找時間打續集 😂
      感謝支持!

      簡單說明:

      1. 實線 + 空心三角形箭頭: 泛化 (Generalization),也就是所謂的繼承。
      2. 虛線 + 空心三角形箭頭: 實作 (Realization),也就是 implements。

      3. 實線 + 實心燕尾箭頭: 關聯 (Association),高階模組 (相依端),擁有低階模組 (獨立端) 的實例作為屬性,
        因此文中的例子中,People 與 Stuffer 的關係,是可以替換為此的。 (強依賴)
      4. 虛線 + 實心燕尾箭頭: 依賴 (Dependency),通常指高階模組的方法或方法參數中,具有低階模組的實例,
        且低階模組改變,會影響高階模組。 是一個很模糊且廣泛的關係,這也是為什麼 UML 會有這麼多 stereotypes 來表達 依賴。
  3. 之前也有讀過相關的文章,但老是覺得沒有吸收進來;謝謝你的分享,直的很簡單易懂。希望以後可以看到更多你的文章~~

  4. 持續關注大大的優質好文
    淺顯易懂真的讓新手受益良多
    很期待你的相關文章哦:)

  5. 感謝鄭大的介紹,非常適合初學者閱讀,但想請問最後People還是依賴Hamburger的實作解法為何? 謝謝

發表迴響