依賴反向原則 (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
階層(hierarchy)
 
 

依賴 (Dependency)

在開始之前,得先知道什麼是『 依賴 (dependency) 』:
依賴,白話就是『需要』,但 為何需要呢?

—— 達到目的、功能

 
即:

X 需要 Y —— > 用來達到 目的 Z

 
例如:

  • 小明需要車車 ——> 脫魯
  • 我需要食物 ——> 填飽肚子
  • 地方的媽媽需要 ——> (誤)

 
以程式為例:

public class Main {
    public static void main(String[] args) {
        // 實例 Person 物件 -- 我
        // 執行 空引數建構子
        Person me = new Person();

        // 開吃囉
        me.eat();
    }
}

class Person {

    private Hamburger burger;
    
    public Person() {
        // 得到一個漢堡
        burger = new Hamburger();
    }
    
    public void eat() {
        // 填飽肚子
        burger.fillUp();
    }
}

class Hamburger  {

    public void fillUp() {
        System.out.println("咔拉雞腿滿福堡 好棒棒");
    }
}

// Result:
// 咔拉雞腿滿福堡 好棒棒

 
解釋:『 我 Me 』依賴『 漢堡 Hamburger 』用來『 填飽肚子 』,
當自身需要呼叫其他類別的實例時,就稱這樣的關係為 — 依賴。
 
 

如果今天不想吃漢堡了,怎辦?

So easy~ 吃麵啊:

public class Main {
    public static void main(String[] args) {
        // 實例 Person 物件 -- 我
        // 執行 空引數建構子
        Person me = new Person();

        // 開吃囉
        me.eat();
    }
}

class Person {

    private Spaghetti spaghetti;

    public Person() {
        // 得到一碗義大利麵
        spaghetti = new Spaghetti();
    }

    public void eat() {
        // 填飽肚子
        spaghetti.fill();
    }
}

class Spaghetti  {
    public void fill() {
        System.out.println("大蒜辣椒麵 :D");
    }

}

class Hamburger  {

    public void fillUp() {
        System.out.println("咔拉雞腿滿福堡 好棒棒");
    }
}

// Result:
// 大蒜辣椒麵 :D

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

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

 


 

依賴反向原則
(Dependency Inversion Principle, DIP)

以上的例子可以看出:

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

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

 
 

名詞解釋

  • 高階與低階是相對的:高階模組表達業務政策或主要規則,低階模組提供資料庫、網路、檔案或外部服務等技術細節;它們不等同於單純的呼叫者與被呼叫者。
    此例中 Person 為高階,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 Filler {
    // 填飽肚子
    void fillUp();
}

 
p.s 你可能疑惑,為什麼不用抽象類別 Food(食物),不是比較簡單好理解嗎?
若需求真的是食物,Food 當然是合理抽象;這裡使用 Filler,是要強調 Person 真正依賴的是「填飽肚子」這個能力,而不是某個食物家族。抽象的名稱應跟著穩定的需求走,不是為了套用介面而硬湊。
 
修改一下程式碼:

public class Main {
    public static void main(String[] args) {
        // 實例 Person 物件 -- 我
        // 執行 空引數建構子
        Person me = new Person();

        // 開吃囉
        me.eat();
    }
}

class Person {

    private Filler filler;

    public Person() {
        // 得到一個能填飽肚子的東西 的實例
        // 實際實作種類是 Hamburger
        filler = new Hamburger();
    }

    public void eat() {
        // 填飽肚子
        filler.fillUp();
    }
}

interface Filler {
    // 填飽肚子
    void fillUp();
}

class Hamburger implements Filler{
    @Override
    public void fillUp() {
        System.out.println("咔拉雞腿滿福堡 好棒棒");
    }
}

// Result:
// 咔拉雞腿滿福堡 好棒棒

 
注意上方 能填飽肚子的東西實例的建構方式是

Filler filler = new Hamburger();

 
而不是!!

Hamburger burger = new Hamburger();

 
你會說:靠邀!講了這麼多
不就只是把前面的類別名稱 改成父類別 (介面)
後面還不是都一樣,用 new Hamburger(); 來建構物件。
 

沒錯 ㄏㄏ
理解此處,正是 介面導向程式設計原則 的第一步

 
僅僅這樣一個小動作:
Person 類別 依賴的對象,變成了抽象、介面 (Filler),而非實際類別 (漢堡、義大利麵、茶)
也就實現了 依賴反向原則:『 要相依於抽象,不要相依於實際類別 』
 
原本的依賴關係:
before_dip_cd
 
改變為:
after_dip_cd
 
 
依賴反轉原則的目的是為了 解除高階模組 (Person) 與低階模組 (Hamburger) 的耦合關係
Person 不再依賴 Hamburger ,而是兩者都依賴 Filler 介面
=> 高階模組不應該依賴於低階模組,兩者都該依賴抽象
=> 抽象不應該依賴於具體實作方式
=> 具體實作方式則應該依賴抽象
 


 

到底哪裡反向了?

原本:高階 –> 低階 (高階依賴低階)
不是應該變成 高階 <– 低階 (低階依賴高階) 才叫『反』嗎?
 
為什麼是變成 兩者都依賴抽象 呢?
 
上述範例中:
原本:人 (高階) 依賴 漢堡、義大利麵.. (低階)
也就是人的行為模式被漢堡、義大利麵綁死了,
漢堡變得比人類大尾,人沒有漢堡,人生就失去了希望。
 
 

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

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

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

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

即:

低階模組依賴抽象,就 間接 的 依賴 了高階模組,
原本 高到低 變成 低到高 的依賴關係,就是 反向 的精神。

 
如果現在有個需求 (抽象),並且有一個具體實作 (低階),
但是沒有需求者 (高階),不就不存在依賴反向嗎?

對啊,阿沒有需求了,那個具體實作是要欉尛?

 


 

擴充

幾乎所有的設計原則或設計模式,都是在談『 改變 』,
讓程式擁有擴充及維護的彈性,成為最重要的課題。
 
由於 Person 依賴 Filler 介面,就算未來替換 Filler 實作子類別 (食物),
也不用更改任何方法呼叫邏輯,確保了 統一的呼叫方式,
也不會像範例中,出現以下的腦殘現象 (不同食物,呼叫方法就不一樣):

hamburger.fillUp(); // 吃漢堡
       vs
spaghetti.fill(); // 吃義大利麵

 
譬如,欲擴充一個新的類別 Tea,只需要實作介面,
高階模組 的 呼叫方式完全一樣:

class Tea implements Filler {

    @Override
    public void fillUp() {
        System.out.println("烙賽 大冰奶");
    }
}

 

使得系統大大的增加了彈性且容易擴充!
使得系統大大的增加了彈性且容易擴充!
使得系統大大的增加了彈性且容易擴充!
— 覺得很重要 o.o

 


 

結語

騙你 der~ 僅僅這樣,依賴反向原則 “尚未”完成,只是為了讓你好理解,
因為還存在一個大問題 — Person 仍必須自己去 new 一個具體實作!

Filler filler = new Tea();

 
也就是高階模組,仍然 依賴著 具體實作:
after_dip_cd(fake)
 
 
這違反了 介面的思想:“封裝隔離 (忘記點我)“,Person 不應知道具體的實作類別是誰,
具體的實作 應是可『 替換 』的:

由 runtime (運行時) 傳入,而非 compile (編譯時) 就決定,
所以又被稱為 —— Plugin (插件)。

 
解決辦法有很多,其實皆是設法,把所需得插件 (低階具體實作元件),提供給高階元件。
讓依賴關係,可以變成理想的:
after_dip_cd
 
世界和平了
 
完整的依賴反向原則,以後再慢慢講嚕 >.^
範例原始檔
 
 

作者: 鄭中勝
喜愛音樂,但不知為何總在打程式 ? 期許能重新審視、整理自身所學,幫助有需要的人。

在〈依賴反向原則 (Dependency-Inversion Principle, DIP)〉中有 30 則留言

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

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

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

      簡單說明:

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

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

    1. 認真感謝! 😭
      嗚嗚嗚 聽了覺得好感動
      我會加油的 謝謝支持 😃

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

    1. 謝謝你 😆
      目前正在進行秘密計畫
      會累積一小段時間
      敬請期待 😂

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

發表迴響