1. 程式人生 > 實用技巧 >依賴注入的通俗講解,設計低耦合的系統

依賴注入的通俗講解,設計低耦合的系統

依賴注入的通俗講解,設計低耦合的系統

依賴注入是一種實現方式,其目的是為了構建低耦合的系統,我用一個簡單的生活中的例子來描述為什麼需要依賴注入,以及依賴注入的好處

先講一講概念:允許從類的外部注入依賴項,因此注入依賴項的類只需要知道一個協定(通常是C#介面)

這句話很抽象,我們可以拿現實中的例子來對比

概念 類比
允許從類的外部注入依賴項 家裡的供電線路接入電器
因此注入依賴項的類只需要知道一個協定(通常是C#介面) 不需要知道是什麼電器,只要是對應的插孔接上就能用

這個概念不一定好理解,我們可以先用生活的例子說明依賴注入的好處,這裡用一個廚房需要電器的例子

沒有依賴注入/沒有電源插頭

修改的生活例子 具體操作
在廚房一角安裝一臺電飯鍋 將電飯鍋的電源線接入供電線,纏上絕緣膠布以防觸電或短路
將電飯鍋更換為微波爐 撕開之前纏上的絕緣膠布,拆除電飯鍋的電源線,更換為微波爐的電源線,重新纏上絕緣膠布以防觸電或短路
將微波爐更換為豆漿機 撕開之前纏上的絕緣膠布,拆除微波爐的電源線,更換為豆漿機的電源線,重新纏上絕緣膠布以防觸電或短路
測試的生活例子 具體操作
測試不工作的豆漿機是供電線問題還是豆漿機問題 撕開之前纏上的絕緣膠布,拆除豆漿機的電源線,更換為其他電器的電源線,重新纏上絕緣膠布以防觸電或短路,工作則是豆漿機問題,不工作則是供電線問題

是的,就是這麼麻煩,修改一次需要電工過來,需要安裝電器裝置的過來,一起折騰一兩個小時的時間,當需求變更,沒有依賴注入/沒有電源插頭就是這麼麻煩,所以我們總是要加班,加不完的班

但是如果有插頭/依賴注入呢

擁有插頭/依賴注入

修改的生活例子 具體操作
將電飯鍋換為微波爐 取下電飯鍋的插頭,插上微波爐的插頭
將微波爐換為豆漿機 取下微波爐的插頭,插上豆漿機的插頭
測試的生活例子 具體操作
測試不工作的豆漿機是供電線問題還是豆漿機問題 取下豆漿機的插頭,插上一個其他正常電器,工作則是豆漿機問題,不工作則是供電線問題

此時問題就變得很簡單,只需要安裝電器裝置的簡單操作一下就解決了問題

這個問題換到軟體開發也是一樣的,如果沒有插頭和插座(介面),問題就變得異常複雜,實際上在軟體開發過程中,真的有很多人不用插頭和插座(介面)

class Boiler
{
    //這個方法可以簡寫為
    // public string Work() => "我是鍋我在燒水";
    public string Work()
    {
        return "我是鍋我在燒水";
    }
}
class Power
{
    public string DoWork()
    {
        Boiler boiler = new Boiler();
        return boiler.Work();
    }
}
class Program
{
    static void Main(string[] args)
    {
        Power power = new Power();
        Console.WriteLine(power.DoWork()); 
    }
}

這就是一個沒有插頭的例子,在Power這個供電線上,直接接上了鍋,如果要有需求變更,則需要在供電線(Power類)上更改DoWork方法,同時在測試中,如果出現了問題,也很難定位到問題是處在Power還是Boiler

這只是一個簡單的示例,只在一個地方用到了Boiler可能看起來好像也只需要更改一點點,但是在大型專案中,可能會幾十次幾百次的用到,那就費時費力了,要是有哪個地方沒改到,問題就更嚴重了

於是在軟體開發中有了依賴注入這一概念

interface IWork
{
    string Work();
}
class Boiler:IWork
{
    //這個方法可以簡寫為
    // public string Work() => "我是鍋我在燒水";
    public string Work()
    {
        return "我是鍋我在燒水";
    }
}
class Power
{
    private readonly IWork _work;
    public Power(IWork work)
    {
        //將work形參賦值給_work欄位,如果work形參是null則丟擲異常
        _work = work ?? throw new ArgumentNullException(nameof(work)); ;
    }
    public string DoWork()
    {
        return _work.Work();
    }
}
class Program
{
    static void Main(string[] args)
    {
        Power power = new Power(new Boiler());
        Console.WriteLine(power.DoWork()); 
    }
}

這個程式碼便是最基本的依賴注入,在這份程式碼中,首先定義了一個IWork的插頭,Boiler滿足IWork的插頭要求

Power中,設計了一個_work插座,接收IWork插頭,在使用時,Power供電給這個插頭就行了

在這個設計中,如果需要將鍋更換為其他電器,只需要在構造Power物件時傳遞實現了這個插頭的其他類即可,無需再改動Power中的程式碼

還是一個表格對比差異

沒有使用依賴注入

修改的需求 具體實現
A類呼叫B類的例項方法 在當前類中new一個物件執行
更換A中呼叫的B類的例項方法為C類的例項方法 修改A類中所有有關B類的引用為C類的引用(可能包含成百上千的引用,一旦漏掉就是嚴重的BUG)
更換A中呼叫的C類的例項方法為D類的例項方法 修改A類中所有有關C類的引用為D類的引用(可能包含成百上千的引用,一旦漏掉就是嚴重的BUG)
測試的需求 現實
A類工作出現嚴重異常 無法準確定位A類錯誤還是其呼叫的底層類錯誤

使用依賴注入

修改的需求 具體實現
A類呼叫B類的例項方法 建立一個介面,A類中呼叫符合介面的方法,構造A類物件時將B類的例項物件傳遞過去
更換A中呼叫的B類的例項方法為C類的例項方法 更改A類物件構造引數中B類例項物件為C類例項物件
更換A中呼叫的C類的例項方法為D類的例項方法 更改A類物件構造引數中C類例項物件為D類例項物件
測試的需求 現實
A類工作出現嚴重異常 編寫簡單的、不出錯的簡單類傳遞給A類測試,若A類正常則是底層類錯誤

使用依賴注入的好處是顯而易見的,尤其是在需求頻繁變更的時候。

0x02 IOC容器(DI容器)

依賴注入很好,但是大型專案中成百上千的依賴如果需要手動注入依然很麻煩,於是有了DI容器(IOC容器)

  • IOC:為相互依賴的元件提供抽象,將依賴(低層模組)物件的獲得交給第三方(系統)來控制,即依賴物件不再被依賴模組的類中直接通過new來獲取

在使用依賴注入時就是使用了IOC設計原理,所以我們說的DI容器其實和IOC容器是一個東西,雖然他們之間的概念有一點差別

IOC容器的主要功能:

  1. 動態建立、注入依賴物件
  2. 管理物件的生命週期
  3. 對映依賴關係

IOC容器(DI容器)下一次單獨拿出來寫,沒有IOC容器的依賴注入看不出使用依賴注入的巨大優勢