我看依賴注入

new代碼味道——狎暱(xia ni)關係:過度親近

這個主題是我比較想重點聊聊的,由於我我的的理解是依賴注入思想最終想解決的問題就是消除對象之間的耦合,再通俗一點講就是消除new代碼味道,解決的指導思想是將組件的配置和使用分離編程

什麼是代碼味道?

  • 若是某段代碼可能存在問題,就能夠說有代碼味道。這裏使用「可能」是由於少許的代碼味道並不必定就是問題。
  • 代碼味道還可能代表有技術債務存在,而技術債務的修復是有代價的。揹負技術債務越久,債務修復就會越難。
  • 代碼味道有許多分類。

思考一下爲何除了一些特殊狀況外,凡是出現new關鍵字的地方都是代碼味道?設計模式

一個示例,展現如何經過實例化對象來破壞代碼的自適應能力瀏覽器

public class AccoutController
{
    private readonly SecurityService securityService;

    public AccountController()
    {
        this.securityService = new SecurityService();
    }

    [HttpPost]
    public void ChangePassword(long userId,string newPassword)
    {
        var userRepository = new UserRepository();
        var user = userRepository.GetById(userId);
        this.securityService.ChangePassword(user,newPassword);
    }
}

複製代碼

這段代碼就比較接近業務代碼了,代碼中有下列一些問題,這些問題是因爲兩個顯式調用new關鍵字的構造對象實例引發的。bash

  • AccoutController類永遠依賴SecurityService類以及UserRepository類的具體實現。
  • AccoutController類隱式依賴SecurityService類以及UserRepository類的全部依賴。
  • AccoutController類很難測試,由於沒法使用僞實現(模擬對象或存根)來模擬和替代SecurityService類和UserRepository類。
  • SecurityService類的ChangePassword方法須要客戶端預選加載好User類的實例對象(變相的依賴)。

詳細剖析一下這幾個問題。 1.沒法加強實現——違反了OCP開閉原則 當咱們想改變SecurityService類的實現時,只有兩種選擇,要麼改動AccountController來直接引用新的實現,要麼給現有的SecurityService類添加新功能。咱們會發現這兩種選擇都很差。第一種選擇違反了對修改關閉,對擴展開放的開閉原則;第二種可能會違反SRP單一職責原則。這樣的代碼沒法加強實現,無異於一錘子買賣。 2.依賴關係鏈——違反了DIP控制反轉原則 AccoutController類依賴SecurityService類,SecurityService類也會有本身的依賴關係。上面的示例代碼SecurityService類可能看起來沒有什麼依賴,可是實際上可能會是這樣:框架

public SecurityService()
{
    this.Session = SessionFactory.GetSession();
}
複製代碼

SecurityService類實際上依賴SessionFactory獲取Session對象,這就意味着AccoutController類也隱式依賴SessionFactory。違反了DIP控制反轉原則:更高層次的模塊不能依賴低層模塊,二者都應該依賴抽象接口或者抽象類。而示例代碼中處處都是對低層模塊的依賴。ide

3.缺少可測試性——違反了代碼的可測試性 代碼的可測試性也很是重要,它須要代碼以必定的格式構建。若是不這樣作,測試將變得極其困難。咱們寫過單元測試必定知道,單元測試第一步即是要對待測試對象進行依賴隔離,只有這樣咱們的測試纔是穩定的(排除了依賴對象的不穩定性)、可重複的。咱們使用的隔離框架moq(實際上是全部隔離框架)都是經過使用模擬實現來替代待測試對象的依賴對象工做的。示例代碼中依賴的對象在代碼編譯階段就已經被肯定了,沒法在代碼運行階段動態的替換依賴對象,因此也就不具有可測試性了。函數

對象構造的替代方法

怎樣作才能夠同時改進AccountController和SecurityService這兩個類,或者其餘任何不合適的對象構造調用呢?如何才能正確設計和實現這兩個類以免上節所講述的任何問題呢?下面有一些互補的方式可供選擇。 1.針對接口編程 咱們首先須要作的改動是將SecurityService類的實現隱藏在一個接口後。這樣AccountController類只會依賴SecurityService類的接口而不是它的具體實現。第一個代碼重構就是爲SecurityService類提取一個接口。 爲SecurityService類提取一個接口:單元測試

public interface ISecurityService
{
    void ChangePassword(long userId,string newPassword);
}

public class SecurityService:ISecurityService
{
    public void ChangePassword(long userId,string newPassword)
    {
        //...
    }
}
複製代碼

下一步就是改動客戶端代碼類調用ISecurityService接口,而不是SecurityService類。 AccountController類如今依賴ISecurityService接口:學習

public class AccountController
{
    private readonly ISecurityService securityService;

    public AccountController ()
    {
        this.securityService = new SecurityService();
    }

    public void ChangePassword(long userId,string newPassword)
    {
        securityService.ChangePassword(userId,newPassword);
    }
}
複製代碼

重構仍然沒有結束,由於依然直接調用了SecurityService類的構造函數,因此重構後的AccountController類依然依賴SecurityService類的具體實現。要將這兩個具體類徹底解耦,還須要做進一步的重構。引入依賴注入(DI)。 2.使用依賴注入 這個主題比較大,沒法用很短的篇幅講完。而且後面咱們會詳細的探討依賴注入,因此如今我只會從使用依賴注入的類的角度來說解一些基本的要點。 繼續咱們的重構,重構後的構造函數代碼部分已經加粗顯示,重構動做的改動很是小,可是管理依賴的能力卻大不相同。AccountController類再也不要求構造SecurityService類的實例,而是要求它的客戶端代碼提供一個ISecurityService接口的實現。 使用依賴注入從AccountController類中移除對SecurityService類的依賴:測試

public class AccountController
{
    private readonly ISecurityService securityService;

    
    public AccountController (ISecurityService securityService)
    {
        if(securityService == null)
        {
            throw new ArgumentNullException("securityService");
        }
        this.securityService = securityService;
    }
    

    public void ChangePassword(long userId,string newPassword)
    {
        this.securityService.ChangePassword(userId,newPassword);
    }
}
複製代碼

本節咱們主要討論了new代碼味道及其缺點,也經過重構代碼的方式引出了new代碼味道兩種互補的方式--針對接口編碼和使用依賴注入。之因此說是互補的方式,是由於針對接口編碼只能讓代碼部分解耦,仍是沒有解決直接調用被依賴類的構造函數的問題;而使用依賴注入雖然解決了這個問題,可是使用依賴注入是依賴於針對接口編程的。能夠說只有咱們針對接口編碼,纔有可能使用依賴注入解決掉new代碼味道

忘記是誰說的了,瞭解學習一件事物以前要先了解學習它的發展歷史。學習任何知識,很重要的一點是學習其中的思惟方式,看待問題,解決問題的思惟方式。因此我但願能經過一個很簡單的小遊戲力求形象的描述依賴注入的演變歷程,以及是什麼推動了依賴注入的演變歷程。但願你們看完以後都能有所收穫,也但願你們看完以後對於依賴注入有本身的理解。讓咱們開始吧!

鴨貓大戰

好了,讓咱們從最簡單的開始,但願咱們能從簡單到複雜,慢慢理解從面向接口編程到依賴注入的思想:
複製代碼

我如今要設計一個鴨貓大戰的遊戲,採用標準的OO技術,首先設計一個鴨子的抽象類。

public abstract class Duck
{
	public void Eat(){};
    public void Run(){};
    public abstract void Display();
}
複製代碼

假設在遊戲中鴨子的吃東西、跑等行爲都是相同的,惟一不一樣的是鴨子的外觀,因此Display方法設置爲抽象的,具體的實如今子類中實現。

public class BeijingDuck:Duck
{
    public override void Display()
    {
        //北京鴨
    };
}

public class ShandongDuck:Duck
{
    public override void Display()
    {
        //山東鴨
    };
}

    //其餘鴨...
複製代碼

好了,如今鴨鴨大戰初版已經上線了。如今產品想讓遊戲中的鴨子能夠叫,最簡單的一種實現方式就是在抽象基類中增長一個Shout()方法,這樣全部的繼承鴨子類型均可以叫了。不過咱們很快就會發現問題來了,這樣作的 話全部的全部的鴨子都會叫了,這顯然是不符合邏輯的。那麼有人確定會想到使用接口了,將Shout()方法提取到 接口中,而後讓會叫的鴨子類型實現接口就能夠了。

public interface IShout
{
    void Shout();
}

public class BeijingDuck:Duck,IShout
{
    public override void Display()
    {
        //北京鴨
    };

    public void Shout()
    {
        //呱呱
    }
}

public class ShandongDuck:Duck,IShout
{
    public override void Display()
    {
        //山東鴨
    };

    public void Shout()
    {
        //呱呱
    }
}
複製代碼

上面的實現看起來很好,可是、可是、可是需求老是在變化的。

如今產品要求鴨子不只要會叫,並且每種鴨子類型叫聲還要求不同,而且不一樣的鴨子類型叫聲還可能會同樣。那麼上面的這種實現當時的缺點就顯示出來了,代碼會在多個子類中重複,而且運行時不能修改(繼承體系的缺點,代碼在編譯時就已經肯定,沒法動態改變)等。

理解爲何要「面向接口編程,而不要面向實現編程」

接下來咱們能夠把變化的地方提取出來,多種行爲的實現用統一的接口實現。當咱們想增長一種行爲時,只須要繼承接口就能夠了,對其它行爲沒有任何影響。

public interface IShout
{
    void Shout();
}

public class GuaGuaShout:IShout
{
    public void Shout()
    {
        //呱呱
    }
}

public class GaGaShout:IShout
{
    public void Shout()
    {
        //嘎嘎
    }
}

    //其餘叫聲行爲類
複製代碼

如今某一種具體的鴨子類型實現就變成了:

public class BeijingDuck:Duck
{
    IShout shout;
    public BeijingDuck(IShout shout)
    {
        this.shout = shout;
    }

    public void Shout()
    {
        shout.Shout();
    }
    //能夠在運行時動態改變行爲
    public void SetShout(IShout shout)
    {
        this.shout = shout;
    }

    public override void Display()
    {
        //北京鴨
    };
}
複製代碼

這樣的設計的優勢就在於能夠在運行時動態的改變行爲,並且在不影響其餘類的狀況下增長更改行爲。因此這樣的設計是充滿彈性的。而對比前面的設計咱們就會發現,以前的設計依賴於繼承抽象類和實現接口,這兩種設計都依賴於「實現」,對象的行爲在編譯完成的那一刻就已經被決定了,沒法改變。(組合優於繼承)

理解爲何要「依賴抽象,而不要依賴具體類」

如今咱們要開始鴨貓遊戲,首先咱們建立一個鴨子對象才能開始遊戲,就像下面這樣。

//建立一隻會呱呱叫的北京鴨
new BeijingDuck(new GuaGuaShout());
//建立一隻會嘎嘎叫的北京鴨
new BeijingDuck(new GaGaShout());
//建立一隻會嘎嘎叫的北京鴨
new ShandongDuck(new GuaGuaShout());
複製代碼

問題又出現了,代碼中充斥着的大量的"new"代碼。當咱們使用「new」的時候,就是在實例化具體類。當出現實體類的時候,代碼就會更缺少「彈性」。越是缺少彈性越是難於改造。在後面咱們還會繼續討論「new代碼味道」。

簡單工廠

讓咱們繼續回到遊戲。爲了增長遊戲的交互性,你能夠選擇鴨或貓中的任一角色開始遊戲。若是咱們選擇了鴨子角色開始遊戲,那麼咱們應該在固定的場景會遇到固定的貓。在波斯會遇到波斯貓,在中國遇到狸花貓,在歐洲遇到挪威森林貓。用簡單工廠不難實現。

public interface ICat
{   
    void Scratch();
}
//波斯貓
public class PersianCat:ICat
{
    public void Scratch()
    {
        //來自波斯貓的撓
    }
}
//狸花貓
public class FoxFlowerCat:ICat
{
    public void Scratch()
    {
        //來自狸花貓的撓
    }
}
//挪威森林貓
public class NorwegianForestCat:ICat
{
    public void Scratch()
    {
        //來自挪威森林貓的撓
    }
}

複製代碼

生產貓的工廠:

public class CatFactory
{
    public ICat GetCat(string catType)
    {
        if(catType.IsNullOrEmpty())
        {
            return null;
        }
        if(catType == "PersianCat")
        {
            return new PersianCat();
        }else if(catType == "FoxFlowerCat")
        {
            return new FoxFlowerCat();
        }else if(catType == "NorwegianForestCat")  
        {
            return new NorwegianForestCat();
        } 
        return null;
    }
}
複製代碼

使用工廠建立貓對象:

public class FactoryPatternDemo 
{
    public static void main(String[] args)
    {
        CatFactory factory = new CatFactory();
        //建立波斯貓對象
        ICat persianCat = factory.GetCat("PersianCat");
        //建立狸花貓對象
        ICat foxFlowerCat = factory.GetCat("FoxFlowerCat");
        //建立挪威森林貓對象
        ICat norwegianForestCat = factory.GetCat("NorwegianForestCat");
    }
}
複製代碼

簡單工廠設計模式屬於建立型模式,它提供了一種建立對象的最佳方式。這是設計模式裏對於工廠模式的說明。 工廠模式確實在必定程度上解決了建立對象的難題,項目中不會再處處充斥了「new代碼味道」。可是有一個問題沒有解決,要實例化哪個對象,是在運行時由一些條件決定。當一旦有變化或擴展時,就要打開這段代碼(工廠實現代碼)進行修改,這違反了「對修改關閉」的原則。還有就是這段代碼依賴特別緊密,而且是高層依賴底層(客戶端依賴具體類(工廠類)的實現),由於判斷建立哪一種對象是在工廠類中實現的。幸運的是,咱們還有「依賴倒置原則」和「抽象工廠模式」來拯救咱們。

抽象工廠和依賴倒置原則

客戶端(高層組件)依賴於抽象Cat,各類貓咪(底層組件)也依賴於抽象Cat,雖然咱們已經建立了一個抽象Cat,可是仍然在代碼中建立了具體的Cat,這個抽象其實並無什麼影響力。使用抽象工廠模式能夠將這些實例化對象的代碼隔離出來。這符合軟件設計中的對於能夠預見變化的部分,要使用接口進行隔離。讓咱們繼續回到遊戲中,以前咱們提到過,在固定的場景會遇到固定的遊戲角色,因此咱們須要爲不一樣遊戲場景建立場景對象。

首先咱們要建立一個工廠接口:

public interface IFactory
{
    ICat CreateCat();
    Duck CreateDuck();
}
複製代碼

而後建立建立場景的工廠類:

public class BeijingSceneFactory:IFactory
{
    public ICat CreateCat()
    {
        return new FoxFlowerCat();
    }

    public Duck CreateDuck()
    {
        return new BeijingDuck();
    }
}

public class ShanDongSceneFactory:IFactory
{
    public ICat CreateCat()
    {
        return new FoxFlowerCat();
    }

    public Duck CreateDuck()
    {
        return new ShanDongDuck();
    }
}
複製代碼

構建場景類(僞代碼):

public class Scene 
{
    private ICat cat;
    private Duck duck;

    IFactory factory;
    public Scene(IFactory factory)
    {
        this.factory = factory;
    }

    public void Create(string role)
    {
        if(role == "duck")
        {
            duck = factory.CreateDuck();
        }
        if(role == "cat")
        {
            cat = factory.CreateCat();
        }
    }
}
複製代碼

這樣一來,遊戲場景改變不禁代碼來改變,而是由客戶端動態的決定。至關於變相的減小了高層對底層的依賴。如今其實咱們已經能大致理解依賴倒置的原則:依賴抽象,而不依賴具體類。 客戶端代碼實現:

public class FactoryPatternDemo 
{
    public static void main(String[] args)
    {
        IFactory factory = new BeijingSceneFactory();
        Scene scene = new Scene(factory);
        scene.Create("duck");
    }
}
複製代碼

如今的代碼設計已經比較靠近「注入」的概念了(窮人的依賴注入),仔細看 Scene scene = new Scene(factory);, 建立場景的工廠對象是抽象的接口類型,並且是經過構造函數動態傳入的,經過這樣的改造就爲咱們使用依賴注入框架提供了可能性。固然在抽象工廠和依賴注入之間,還有一個問題值得咱們去思考。這個問題就是「如何將組件的配置和使用分離」,答案也已經很明瞭了——依賴注入。

理解將組件的配置和使用分離

若是以爲組件這個比較抽象的話,咱們能夠把「組件」理解爲「對象」(底層組件),那麼相應的「組件的配置」就理解成爲「對象的初始化」。如今「將組件的配置和使用分離」這句話就很好理解了 ,就是將對象的建立和使用分離。這樣作的優勢很明顯,將對象的建立推遲到了部署階段(這句話可能不太好理解),就是說對象的建立所有依賴於咱們統一的配置,咱們能夠修改配置動態的把咱們不想使用的對象替換成咱們想使用的對象,而不用修改任何使用對象的代碼。原則上咱們須要把對象的裝配(配置)和業務代碼(使用)分離開來。

依賴注入

依賴注入(DI)是一個很簡單的概念,實現起來也很簡單。儘管如此,這種簡單性卻掩蓋了該模式的重要性。當某些事情很簡單也很重要時,人們就會將它過分複雜化,依賴注入也同樣。要理解依賴注入,咱們首先要這個詞拆開來解讀——依賴和注入。

什麼是依賴?

要用文字解釋這個概念可能不太好理解(文不如表,表不如圖),咱們可使用有向圖對依賴建模。一個依賴關係包含了兩個實體,它們之間的聯繫方向是從依賴者到被依賴者

使用有向圖對依賴建模: A依賴B:

B依賴A:

互聯網提供不少服務,服務依賴互聯網:

包(包括程序集和命名空間)既是客戶也是服務:

客戶端類依賴服務類:

有些服務會隱藏在接口後面:

有向圖中有一種特殊的循環叫作自循環

方法層的遞歸就是一個很好的自循環的例子。

軟件系統中的依賴

咱們都知道,在採用面向對象設計的軟件系統中,萬物皆對象。全部的對象經過彼此的合做,完成整個系統的工做。就比如下面的齒輪系統,每一個齒輪轉動帶動整個齒輪系統的運轉。可是這樣的設計就意味着強依賴,強耦合。若是某個齒輪出問題不轉動了,整個齒輪系統就會癱瘓掉,這顯然是咱們所不能接受的。

圖1.軟件系統中耦合的對象:

什麼是控制反轉(IOC)?

耦合關係不只會出如今對象與對象之間,也會出如今軟件系統的各模塊之間,以及軟件系統和硬件系統之間。如何下降系統之間、模塊之間和對象之間的耦合度,是軟件工程永遠追求的目標之一。爲了解決對象之間的耦合度太高的問題,軟件專家Michael Mattson提出了IOC理論,用來實現對象之間的「解耦」。目前這個理論已經被成熟的應用到項目當中,衍生出了各式各樣的IOC框架產品。 IOC理論提出的觀點大體是這樣的:藉助於「第三方」實現具備依賴關係的對象之間的解耦。以下圖: 圖2.IOC解耦過程:

因爲引進了中間位置的「第三方」,也就是IOC容器,使得A、B、C、D這4個對象沒有了耦合關係,齒輪之間的傳動所有依靠「第三方」了,所有對象的控制權所有上繳給「第三方」IOC容器,因此,IOC容器成了整個系統的關鍵核心,它起到了一種相似「粘合劑」的做用,把系統中的全部對象粘合在一塊兒發揮做用,若是沒有這個「粘合劑」,對象與對象之間會彼此失去聯繫,這就是有人把IOC容器比喻成「粘合劑」的由來。 那麼若是咱們把IOC容器拿掉,系統會是什麼樣子呢? 圖3.拿掉IOC容器的系統:

拿掉IOC容器的系統,A、B、C、D這4個對象之間已經沒有了耦合關係,彼此毫無聯繫,這樣的話,當你在實現A的時候,根本無須再去考慮B、C和D了,對象之間的依賴關係已經下降到了最低程度。

軟件系統在沒有引入IOC容器以前,如圖1所示,對象A依賴於對象B,那麼對象A在初始化或者運行到某一點的時候,本身必須主動去建立對象B或者使用已經建立的對象B。不管是建立仍是使用對象B,控制權都在本身手上。軟件系統在引入IOC容器以後,這種情形就徹底改變了,如圖3所示,因爲IOC容器的加入,對象A與對象B之間失去了直接聯繫,因此,當對象A運行到須要對象B的時候,IOC容器會主動建立一個對象B注入到對象A須要的地方。經過先後的對比,咱們不難看出來:對象A得到依賴對象B的過程,由主動行爲變爲了被動行爲,控制權顛倒過來了,這就是「控制反轉」這個名稱的由來。

什麼是依賴注入?

2004年,Martin Fowler探討了同一個問題,既然IOC是控制反轉,那麼究竟是「哪些方面的控制被反轉了呢?」,通過詳細地分析和論證後,他得出了答案:「得到依賴對象的過程被反轉了」。控制被反轉以後,得到依賴對象的過程由自身管理變爲了由IOC容器主動注入。因而,他給「控制反轉」取了一個更合適的名字叫作「依賴注入(Dependency Injection)」。他的這個答案,實際上給出了實現IOC的方法:注入。所謂依賴注入,就是由IOC容器在運行期間,動態地將某種依賴關係注入到對象之中。 因此如今咱們知道,控制反轉(IOC)和依賴注入(DI)是從不一樣角度對同一件事物的描述。就是經過引入IOC容器,利用注入依賴關係的方式,實現對象之間的解耦。

使用控制反轉(IOC)容器

咱們在開發時常常會遇到這種狀況,開發中的類委託某些抽象完成動做,而這些被委託的抽象又被其餘的類實現,這些類又委託其餘的一些抽象完成某種動做。最終,在依賴鏈終結的地方,都是一些小且直接的類,它們已經不須要任何依賴了。咱們已經知道如何經過手動構造類實例並把它們傳遞給構造函數的方式來實現依賴注入的效果(窮人的依賴注入)。儘管這種方式能夠任意替換依賴的實現,可是構造的實例對象圖依舊是靜態的,也就是說編譯時就已經肯定了。控制反轉容許咱們將構建對象圖的動做推遲到運行時。 控制反轉容器組成的系統可以將應用程序使用的接口和它的實現類關聯起來,並能在獲取實例的的同時解析全部相關的依賴。 示例代碼中沒有手動構造實現的實例,而是經過使用Unity控制反轉容器來創建類和接口的映射關係:

public partial class App:Application
{
    private IUnityContainer container;
    private void OnApplicationStartUp()
    {
        container = new UnityContainer();

        container.RegisterType<ISettings,ApplicationSettings>();
        container.RegisterType<ITaskService,TaskService>();

        var taskService = container.Resolve<ITaskService>();
    }
}
複製代碼

1.代碼的第一步就是初始化獲得一個UnityContainer實例。 2.在建立好Unity容器後,咱們須要告訴該容器應用程序生命週期內每一個接口對應的具體實現類是什麼。Unity遇到任何接口時,都會知道去解析哪一個實現。若是咱們沒有爲某個接口指定對應的實現類,Unity會提醒咱們該接口沒法實例化。 3.在完成接口和對應實現類的關係註冊後,咱們須要得到一個TaskService類的實例。Unity容器的Resolve方法會檢查TaskService類的構造函數,而後嘗試去實例化構造函數要注入的依賴項。如此反覆,直到徹底實例化整個依賴鏈上的全部依賴項的實例後,Resolve方法會成功實例化TaskService類的實例。

控制反轉(IOC)容器的工做模式——註冊、解析、釋放模式

全部的控制反轉容器都符合一個只有三個的方法的簡單接口,Unity也不例外。 儘管每一個控制反轉容器實現不徹底相同,可是都符合下面這個通用的接口:

public interface IContainer:IDisposable
{
    void Register<TInterface,TImplementation>()
        where TImplementation:TInterface;

    TImplementation Resolve<TInterface>();

    void Release();
}
複製代碼
  • Register:應用程序首先會調用此方法。並且該方法會被屢次調用以註冊不一樣的接口及其實現之間的映射關係。這裏的Where子句用來強制TImplementation類型必須實現它所繼承的TInterface接口。
  • Resolve:應用程序運行時會調用此方法獲取對象實例。
  • Release:應用程序生命週期中,當某些類的的實例再也不須要時,就能夠調用此方法釋放它們佔用的資源。這有可能發生在應用程序結束時,也有可能發生在應用程序運行的某個恰當時機。 咱們都知道在咱們使用的Unity容器註冊時能夠配置是否開啓單例模式。一般狀況下,資源只對單次請求有效,每次請求後都會調用Release方法。可是當咱們配置開啓單例模式時,只有在應用程序關閉時纔會調用Release方法。

命令式與聲明式註冊

到此爲止,咱們都是使用的命令式註冊:命令式的從容器對象上調用方法。 命令式註冊優勢:

  • 比較簡潔,易讀。

  • 編譯時檢查問題的代價很是小(好比防止代碼輸入錯誤等)。 命令式註冊缺點:

  • 註冊的過程在編譯時已經肯定了,若是想要替換實現,必須修改源代碼,而後從新編譯。

若是經過XML配置進行聲明式註冊,就不須要從新編譯。 應用程序配置文件:

<configuration>
    <configSections name="unity" 
    type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection">
    </configSections>
    <unity>
        <container>
            <register type="ISettings" mapTo="ApplicationSettings"/>
            <register type="ITaskService" mapTo="TaskService"/>
        </container>
    </unity>
</configuration>

複製代碼

應用程序入口:

public partial class App:Application
{
    private IUnityContainer container;
    private void OnApplicationStartUp()
    {
        var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
        container = new UnityContainer().LoadConfiguration(section);

        var taskService = container.Resolve<ITaskService>();
    }
}
複製代碼

聲明式註冊優勢:

  • 將接口和相應的實現的映射動做推遲到配置時。 聲明式註冊缺點:
  • 太繁瑣,配置文件會巨大。
  • 註冊時的錯誤會跳過編譯,直到運行時才能被發現和捕獲。

三種依賴注入方式及其優缺點

首先你們思考一下爲何在項目中會要求你們在控制器層使用屬性注入,在業務邏輯層使用構造函數注入?

1.構造函數注入

public class TaskService
{
    private ITaskOneRepository taskOneRepository;
    private ITaskTwoRepository taskTwoRepository;
    public TaskService(
        ITaskOneRepository taskOneRepository,
        ITaskTwoRepository taskTwoRepository)
        {
            this.taskOneRepository = taskOneRepository;
            this.taskTwoRepository = taskTwoRepository;
        }
}
複製代碼

優勢:

  • 在構造方法中體現出對其餘類的依賴,一眼就能看出這個類須要其餘那些類才能工做。
  • 脫離了IOC框架,這個類仍然能夠工做(窮人的依賴注入)。
  • 一旦對象初始化成功了,這個對象的狀態確定是正確的。

缺點:

  • 構造函數會有不少參數。
  • 有些類是須要默認構造函數的,好比MVC框架的Controller類,一旦使用構造函數注入,就沒法使用默認構造函數。

2.屬性注入

public class TaskService
{
    private ITaskRepository taskRepository;
    private ISettings settings;
    public TaskService(
        ITaskRepository taskRepository,
        ISettings settings)
        {
            this.taskRepository = taskRepository;
            this.settings = settings;
        }
    public void OnLoad()
    {
        taskRepository.settings = settings;
    }
}
複製代碼

優勢:

  • 在對象的整個生命週期內,能夠隨時動態的改變依賴。
  • 很是靈活。

缺點:

  • 對象在建立後,被設置依賴對象以前這段時間狀態是不對的(從構造函數注入的依賴實例在類的整個生命週期內均可以使用,而從屬性注入的依賴實例還能從類生命週期的某個中間點開始起做用)。
  • 不直觀,沒法清晰地表示哪些屬性是必須的。

3.方法注入

public class TaskRepository
{
    private ISettings settings;

    public void PrePare(ISettings settings)
    {
        this.settings = settings;
    }
}
複製代碼

優勢:

  • 比較靈活。

缺點:

  • 新加入依賴時會破壞原有的方法簽名,若是這個方法已經被其餘不少模塊用到就很麻煩。
  • 與構造方法注入同樣,會有不少參數。

相信你們如今必定理解了項目中某一層指定某一種注入方式的緣由:利用其優勢,規避其缺點。

組合根和解析根

1.組合根 應用程序中只應該有一個地方直到依賴注入的細節,這個地方就是組合根。在使用窮人的依賴注入時就是咱們手動構造類的地方,在使用控制反轉容器時就是咱們註冊接口和實現類間映射關係的地方。組合根提供了一個查找依賴注入配置的公認位置,它能幫你避免把對容器的依賴擴散到應用程序的其餘地方。 2.解析根 和組合根密切相關的一個概念是解析根。它是要解析的目標對象圖中根節點的對象類型。 這樣講很抽象,舉個例子: MVC應用程序的解析根就是控制器。來自瀏覽器的請求都會被路由到被稱爲動做(action)的控制器方法上。每當請求來臨時,MVC框架會將URL映射爲某個控制器名稱,而後找到對應名稱的類實例化它,最後在該實例上觸發動做。更確切的講,實例化控制器的過程就是解析控制器的過程。這意味着,咱們能輕易的按照註冊、解析和釋放的模式,最小化對Resolve方法的調用,理想情況下,就只應該在一個地方調用該方法。

組合根和解析根又是前面所講的「將組件的配置和使用分離」一種體現。

依賴注入的技術點

IOC中最基本的技術就是「反射(Reflection)」編程。有關反射的相關概念你們應該都很清楚,通俗的講就是代碼運行階段,根據給出的信息動態的生成對象。

總結

作一下總結,咱們從new代碼味道出發,引出了消除new代碼味道(代碼解耦)的兩種方式——針對接口編碼和使用依賴注入。而後咱們經過開發一個小遊戲,瞭解了面向接口編程到依賴注入的歷程。最後深刻了介紹了大Boss——控制反轉(依賴注入),主要介紹了什麼是依賴,控制反轉(依賴注入)的概念,使用控制反轉(IOC)容器,工做模式,命令式與聲明式註冊,三種依賴注入方式及其優缺點,組合根和解析根,依賴注入的技術點。 本次分享力求從原理和思想層面剖析依賴注入。由於我水平有限,可能有些點講的有些片面或不夠深刻,因此給出我準備此次分享的參考資料。有興趣深刻研究的同窗,能夠自行去看一下這些資料: 1.C#敏捷開發實踐 第2章 依賴和分層 第9章 依賴注入原則

2.HeadFirst設計模式 鴨貓大戰改編自第一章 設計模式入門


                                                     -----END-----


              喜歡本文的朋友們,歡迎掃一掃下圖關注公衆號擼碼那些事,收看更多精彩內容

                                       

相關文章
相關標籤/搜索