在聊依賴反轉之前,先讓我們聊聊什麼是依賴,所謂的依賴就是一種「受到某個東西影響、牽制」的狀態。

例如說如果有個像我一樣的肥宅每天一定要來一片雞排才能療癒身心,那我就是依賴雞排;
同樣的,如果有個大叔不抽菸就會全身不舒服,就是對香菸有所依賴。

當有「必須要藉由某個人事物來達到目的」的狀況時,就是依賴。

而在程式設計裡面的概念也差不多,如果A模組直接受到B模組的影響,我們就稱A依賴了B,最明顯的狀況就是A模組需要藉由B模組的實例來完成某個功能的時候。

例如「匯出報表」功能建立了一個「Excel 控制類別」的實例以建立檔案;
或是「會員查詢」功能建立了一個「DB 連線」的實例來進入資料庫取得會員資料

遇見這種「必須要藉由某個模組的實例來完成想要的動作」的狀況時,就是依賴。

依賴與耦合

我們在之前的 耦合篇 提過,如果模組和另一個模組之間有關連,那這兩者之間就耦合。以此來看,依賴就是一種耦合的關係,那麼,依賴是健康還是不健康的耦合呢?

現在讓我們用 多型篇 用過的「老闆徵工程師」的例子來舉例一下:現在有間小小公司,老闆請來了小明當工程師,並請他開工撰寫產品程式碼。

當「撰寫產品程式」對「工程師」直接依賴的時候,狀況可能是這樣的:

public Product Work()
{
    Ming programmer = new Ming();
    var product = programmer.Programming();
    return product;
}

過一陣子,老闆發現小明寫出來的東西似乎不太行,於是把小明趕走,另外請了小華。這時候因為「工程師」這個實作類別不一樣了,我們就必須要改一次程式碼:

public Product Work()
{
    Hua programmer = new Hua();
    var product = programmer.Programming();
    return product;
}

又過了好一陣子,老闆又另外請了小美來工作。於是又要再改一次,而且小美的工作方式甚至不叫做 Programming,而是 Coding

public Product Work()
{
    Mei programmer = new Mei();
    var product = programmer.Coding();
    return product;
}

現在有感覺到一點問題了嗎?如果一直換人,Work 的程式碼豈不是每次都要修改?甚至根據依賴對象的不同,連使用方式都可能受到影響,很明顯這樣就是所謂不健康的耦合。

這邊還有個例子我很喜歡,在這篇 依賴倒置原則 的文章中,用吃東西來舉例:如果寫死了依賴漢堡,難道一輩子就只能吃漢堡了嗎?如果想改成吃義大利麵,就要修改程式碼;有一百種食物,難道就要改一百次嗎?

試想,因為「DB 的連線方法」有了一些變更,使用到該方法的「會員查詢」就連帶要更動,甚至有關聯的地方都必須要變動,如此一來改動的範圍如森林大火般延燒。

同時由於我們在開發功能的時候,都是讓大功能(高階模組)調用各個小功能(低階模組)來實現目標,越高層的就越整體、越抽象、越接近目標;而越低階就越細節、越接近實作,關注點越小。而我們的思維通常是由大範圍往下到小實作,從整體目標逐漸拆解成各個步驟。

但是,當我們的高階模組直接依賴低階模組的時候,事情就會變得有點怪怪的。就像董事長必須清潔廁所導致沒空進行公司決策一樣,原本職責在於高層次、整體的模組,卻不得不因為這些低階模組的變動受到影響。那麼隨著層次越高,底下依賴的模組越多,改動的頻率就會提高。

既然改動範圍又大,改動頻率又高,耦合又不健康,就代表這樣的依賴是有問題的。然而,物件導向的精神就在於讓物件之間互相協作,消除多餘的重複。因此,依賴又是不可能消除的。

依賴反轉原則 (Dependency-Inversion Principle)

面對這樣的困境,依賴反轉原則告訴我們:

高階模組不應該依賴於低階模組。兩者都應該依賴抽象。

是的,不應該直接去依賴,而是必須藉由抽象來隔開。不應該直接去受到實作的影響,而是只要關注在所需要的功能。

這部分其實已經破梗完了,我們在 介面篇 已經說明過依賴反轉最基本的思維路線。我們並不是用低階模組的功能直接拼湊出高階模組,讓高階模組直接依賴低階模組然後受到影響;而是把關注點放在需要的功能上,用介面隔開實作,解開他們彼此之間的耦合,介面就是模組之間的抽象層。

同時也要明白一件重要的事:並不是高階模組去依賴低階模組。而是高階模組提出它需要的功能,低階模組去實作出這些功能、達成高階模組的目標

我們並不是因為有「DB 的連線方法」和「處理會員資料的方法」所以才說「我們有這兩個東西欸,那我們來組成會員查詢功能吧」;而是「我們想做一個會員查詢功能,所以我們需要連線到 DB,然後對這些資料做篩選和處理」

就如同我們在介面的例子所提的一樣:「老闆為了製造產品(高階模組的目標),開出了工程師的應徵條件(介面),而小明前來應徵(低階模組的實作)」

如此一來,依賴就「反轉」了。原本是 高階模組 → 低階模組 的關係,變成了 高階模組 → 介面 ← 低階模組。並不是高階去依賴低階,而是低階去依賴高階要求的功能。

這也就是依賴反轉原則的第二點:

抽象不應該依賴細節;細節應該依賴抽象。

到這邊我們就推完 介面篇 的前提了,請大家再回顧一下介面篇的內容。也就是說,上面的例子改用抽象層隔離之後,就會和介面篇的例子相同,變成:

public Product Work()
{
    IProgrammer programmer = new Ming();
    var product = programmer.Programming();
    return product;
}

這邊就會遇到我們介面篇結束時所問的問題:我們使用功能之前,必須先建立該類別的實例,也就是 new Ming(),那麼,我們不就還是直接依賴了實作嗎?

控制反轉 (Inversion of Control, IoC) &
依賴注入 (Dependency Injection)

即使我們反轉了依賴關係,但總是要建立實例才能使用的呀。所以,只是將對具體的依賴更改為對抽象的依賴,仍然是不夠的,在要使用的瞬間就會遭遇到問題。面對這個問題,大大們提出了許多個解決的方法,今天就介紹一個比較常見的方向:控制反轉 (Inversion of Control, IoC)

思路非常的簡單:既然如此,我們把實例的建立和實例的使用切分開來就好了,不再是由高階模組去建立並控制低階模組,而是我們讓一個控制反轉中心去建立低階模組,然後高階模組要使用的時候再把這個低階模組交給高階模組使用

如此一來,控制權也跟著反轉過來了,高階模組從主動建立低階模組,變成被動接收低階模組;也就是從原先的 高階模組 —(建立)→ 低階模組,變成了 高階模組 ←(傳遞低階模組)— 控制反轉中心

控制反轉的概念比較像是:當肚子餓的時候,如果自己煮菜的時候,必須自己備料、自己烹調、才能有東西吃。但如果去餐廳點餐,只要說出自己想要的餐點,店家就會負責備料,廚師就會烹調,最後就把需要的餐點送上桌來吃。

也就是說,高階模組再也不需要關心如何建立,該建立哪個實體,只專注於使用功能,真正達到介面的精神。低階模組也只需要等待控制反轉中心分發,到了崗位就把份內事做好,專心在自己的職責身上即可。如此一來就能解除兩者之間的耦合。

但是,要怎麼把控制中心建立的低階模組,交給高階模組做使用呢?這時候的實作方式就是我們所謂的 依賴注入 (Dependency Injection) 了。

依賴注入說穿了很簡單,就是用各種姿勢把東西丟進去給類別使用

例如說我們先前提過的 建構式,就是其中一種解決方法。用上面的例子,就會變成:

public class ProductService
{
    private IProgrammer _programmer;

    public ProductService(IProgrammer programmer)
    {
        this._programmer = programmer;
    }

    public Product Work()
    {
        var product = this._programmer.Programming();
        return product;
    }
}

在這個例子中,我們利用建構式的方式,從外部傳入該介面的實體來使用。現在撰寫產品程式碼的工作再也不用為了換工程師而改變,也不用因為實作細節或是方法名稱而煩惱,只要照個介面合約使用就可以了。至於要傳遞哪個實體進來,這份工作要交給小明還是小美,就讓控制中心去決定,大家各司其職,落實單一職責。

當然,注入的方式不只建構式注入,還有設值注入(也就是從外部改變目標的某個屬性值來達到注入)等等;提供 IOC 的方式也不只一種,例如 .net 的 Unity,甚至到了 .net Core 時代 IOC 還直接是內建的功能呢,由於口味眾多,此處暫且按下不表。

補充:下個系列文補了 使用 依賴注入 (Dependency Injection) 來解除強耦合吧,有興趣的朋友可以接續看看

結語

那麼,我們最後再來複習一遍:

高階模組不應該依賴於低階模組,兩者都應該依賴抽象。為了解除耦合,必須用介面這種抽象層進行隔離。

抽象不應該依賴細節。細節應該依賴抽象。介面應該是高階模組提出的要求,然後才去使用實作了這些要求的低階模組。這些實作應該圍繞著這些要求,而不是讓要求去配合實作,更不要讓要求中包含實作。

為了解決介面實例化仍然會產生依賴的問題,就有了控制反轉。把控制權交給第三方,藉此讓使用者能夠不用關心實例化的過程,而注重在使用並達成目標的職責上。

而控制反轉的具體實現方法是依賴注入,藉由從建構式傳遞、更改目標的屬性等方式,把低階模組交給高階模組使用者。當我們藉由依賴注入的方式實現控制反轉,就能夠讓物件的設計符合依賴反轉原則。

這個部份的做法還是挺複雜的,所以才拖稿這麼久,因此決定把原因的順序推過一遍,也算是幫自己重新了解一次。參考資料有蠻多篇我都相當喜歡,想更了解依賴反轉、控制反轉等等的朋友可以再自行閱讀。那麼,我們下次見~

本系列下一篇:菜雞與物件導向 (15): 最少知識原則

參考資料

同系列文章