自救必看三大準則

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

又稱為:相依性反向、依賴反轉原則,是物件導向系統程式中,
五個基礎設計原則 『 S.O.L.I.D 』中的 “” (DIP),是一種 特定的解耦 形式。
是為了:
『 解除 高階模組 (Caller 呼叫者) 與 低階模組 (Callee 被呼叫者)的 耦合關係,
使高階模組不再直接依賴低階模組。 』




階層(hierarchy)
 
聽起來有點鏘… 但其實超好理解 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


聰明如你,應該看出問題所在了
依賴產生的問題:
  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 (插件)。
解決辦法有很多,其實皆是設法,把所需得插件 (低階具體實作元件),提供給高階元件。
讓依賴關係,可以變成理想的:



世界和平了

完整的 依賴倒置原則 ,以後再慢慢講嚕 >.^


範例原始檔

留言

這個網誌中的熱門文章

IIS - ASP.NET 網站基本優化設定

Node.js 部署至 IIS 站台

遇見 Parameters 參數上限之大量資料寫入方法