c# - 為什麼要使用依賴注入?


我試圖理解依賴注入 (DI),並再次失敗。 這似乎很愚蠢。 我的代碼從來不是一團糟。 我幾乎不寫虛擬函數和接口(雖然我只做了一次藍月亮),所有的配置都被神奇地用json.net(有時使用XML序列化器)序列化成一個類。

我不太明白它解決了什麼問題。 它看起來像一個方式來說:“嗨,當你遇到這個函數,返回一個這種類型的對象,並使用這些參數/數據。
但是...為什麼我會用這個? 注意我從來不需要使用object ,但是我明白這是什麼意思。

在建立一個使用DI的網站或桌面應用程序時,有什麼真實的情況? 我可以很容易地想出為什麼有人可能想要在遊戲中使用接口/虛擬函數的情況,但是在非遊戲代碼中使用它非常少見(罕見的是我不記得單個實例)。



Answers


首先,我想解釋一下我為這個答案所做的一個假設。 這並不總是如此,但經常是這樣:

接口是形容詞; 類是名詞。

(實際上,也有名詞的界面,但我想在這裡概括一下。)

所以,例如一個接口可能是諸如IDisposableIEnumerableIPrintable 。 一個類是一個或多個這些接口的實際實現: ListMap可能都是IEnumerable實現。

為了得到這個觀點:通常你的課程是相互依賴的。 例如,你可以有一個Database類來訪問你的數據庫(哈,驚喜!--)),但是你也希望這個類做訪問數據庫的日誌記錄。 假設你有另一個類Logger ,那麼DatabaseLogger有一個依賴。

到現在為止還挺好。

您可以使用以下行在您的Database類中對此依賴關係進行建模:

var logger = new Logger();

一切都很好 直到你意識到你需要一堆記錄器的時候,這是沒問題的:有時你想登錄到控制台,有時到文件系統,有時使用TCP / IP和遠程日誌記錄服務器,等等。

當然,你不想改變你所有的代碼(同時你有它的代碼)並且替換所有的代碼

var logger = new Logger();

通過:

var logger = new TcpLogger();

首先,這並不好玩。 其次,這是容易出錯的。 第三,對於訓練有素的猴子來說,這是愚蠢的,重複的工作。 所以你會怎麼做?

顯然,引入一個由所有各種記錄器實現的接口ICanLog (或類似的)是個不錯的主意。 所以你的代碼中的第1步是你:

ICanLog logger = new Logger();

現在類型推斷不再改變類型,你總是有一個單一的界面來發展。 下一步是你不想一次又一次地擁有new Logger() 。 所以,你把可靠性創建到一個單一的中央工廠類,你得到的代碼如下:

ICanLog logger = LoggerFactory.Create();

工廠本身決定創建什麼樣的記錄器。 您的代碼不再在意,如果您想更改正在使用的記錄器的類型,請將其更改一次 :在工廠內部。

現在,當然,你可以概括這個工廠,並使其適用於任何類型:

ICanLog logger = TypeFactory.Create<ICanLog>();

在某處,這個TypeFactory需要配置數據,當請求一個特定的接口類型時,實際的類將被實例化,所以你需要一個映射。 當然,你可以在代碼中做這個映射,但是類型改變意味著重新編譯。 但是你也可以把這個映射放在一個XML文件中,例如。 這使您可以在編譯時(!)之後更改實際使用的類,這意味著可以動態地重新編譯!

給你一個有用的例子:想想一個沒有正常記錄的軟件,但是當你的客戶打電話問他求助,因為他有問題,你發給他的是一個更新的XML配置文件,現在他已經日誌啟用,您的支持可以使用日誌文件來幫助您的客戶。

而現在,當你取代一些名字時,你最終得到了一個服務定位器的簡單實現,它是反轉控制的兩種模式之一 (因為你反過來控制誰來決定要實例化的確切的類)。

總而言之,這減少了代碼中的依賴關係,但是現在所有的代碼都依賴於中央的單一服務定位器。

依賴注入現在是這一行的下一步:只要擺脫這個單一的依賴到服務定位器:而不是各種各樣的類要求服務定位器的具體接口的實現,你再次 - 控制誰實例化什麼。

使用依賴注入,你的Database類現在有一個構造函數,需要一個ICanLog類型的參數:

public Database(ICanLog logger) { ... }

現在你的數據庫總是有一個記錄器來使用,但是它不知道這個記錄器來自哪裡。

而這正是DI框架發揮作用的地方:您再次配置您的映射,然後請您的DI框架為您實例化您的應用程序。 由於Application類需要一個ICanPersistData實現,所以注入了一個Database實例 - 但是為此,它必須首先創建一個為ICanLog配置的記錄器實例。 等等 ...

所以,簡而言之就是:依賴注入是如何去除代碼中依賴關係的兩種方法之一。 這對於編譯後的配置更改是非常有用的,對於單元測試來說是一件好事(因為它使得注入存根和/或模擬變得非常簡單)。

在實踐中,沒有服務定位器是不能做的事情(例如,如果事先不知道你需要多少個實例需要一個特定的接口:一個DI框架每個參數總是只注入一個實例,但是你可以調用當然是一個循環內的服務定位器),因此大多數情況下每個DI框架都提供一個服務定位器。

但基本上就是這樣。

希望有所幫助。

PS:我在這裡描述的是一種叫做構造函數注入的技術,還有屬性注入 ,其中不是構造函數參數,而是用於定義和解決依賴關係的屬性。 將屬性注入視為可選的依賴項,將構造函數注入視為必需的依賴項。 但是這個問題的討論超出了這個問題的範圍。




我想很多時候人們會對依賴注入和依賴注入框架 (或者一個經常被調用的容器 )之間的區別感到困惑。

依賴注入是一個非常簡單的概念。 而不是這個代碼:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

你寫這樣的代碼:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

就是這樣。 認真。 這給了你很多好處。 兩個重要的功能是從中心位置( Main()函數)控制功能,而不是將其分散到整個程序中,並且可以更容易地單獨測試每個類(因為您可以將模擬對像或其他偽造對像傳入它的構造函數而不是真正的價值)。

當然,缺點是你現在有一個知道你的程序使用的所有類的超級函數。 這是DI框架可以幫助的。 但是如果你不明白為什麼這個方法是有價值的,我建議先從手動依賴注入開始,這樣你可以更好地理解那裡的各種框架可以為你做什麼。




正如其他答案所述,依賴注入是一種在使用它的類之外創建依賴的方法。 你從外面注入他們,並把他們的創作從班級裡面拿走。 這也是為什麼依賴注入是控制反轉 (IoC)原理的一個實現。

IoC是原則,其中DI是模式。 根據我的經驗,你可能“需要多個記錄器”的原因實際上從來沒有遇到過,但實際的原因是,當你測試某些東西時,你確實需要它。 一個例子:

我的特色:

當我看到一個提議,我想標記,我自動看著它,所以我不會忘記這樣做。

你可能會這樣測試:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

因此,在OfferWeasel某個地方,它會為您創建一個對象,如下所示:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

這裡的問題是,這個測試很可能總是失敗,因為被設置的日期與被聲明的日期不同,即使你只是把DateTime.Now放在測試代碼中,它可能會被關閉幾毫秒並因此總是失敗。 現在一個更好的解決方案將是為此創建一個接口,它允許您控制將設置的時間:

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

接口是抽象的。 一個是真實的東西,另一個允許你假冒一些需要的地方。 測試可以像這樣改變:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

像這樣,你通過注入依賴(獲取當前時間)來應用“控制反轉”原理。 這樣做的主要原因是為了更簡單的單元測試,還有其他的方法。 例如,一個接口和一個類在這裡是不必要的,因為在C#中函數可以作為變量傳遞,所以可以使用Func<DateTime>來實現相同的接口。 或者,如果採取動態方法,只需傳遞任何具有等效方法的對象( 鴨子鍵入 ),並且根本不需要接口。

你幾乎不需要多於一個記錄器。 儘管如此,依賴注入對於靜態類型代碼(例如Java或C#)是必不可少的。

而且......還應該注意的是,一個對像只能在運行時正確地實現它的目的,如果它的所有依賴關係都可用的話,那麼在建立屬性注入方面就沒有多少用處了。 在我看來,當構造函數被調用時,所有的依賴關係都應該被滿足,所以構造函數注入是一個需要解決的問題。

我希望有所幫助。




我認為經典的答案是創建一個更耦合的應用程序,它不知道在運行時將使用哪個實現。

例如,我們是一家中央支付提供商,與世界各地的許多支付提供商合作。 但是,當提出請求時,我不知道要撥打哪個付款處理器。 我可以用一大堆的開關盒來編程,例如:

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

現在想像一下,現在您需要將所有這些代碼保存在一個類中,因為它沒有正確分離,您可以想像,對於您將支持的每個新處理器,您需要創建一個新的if // switch每一種方法,只是通過使用依賴注入(或者控制反轉 - 因為它有時被稱為,意味著控製程序運行的人只有在運行時才知道,而不是複雜的),這只會變得更加複雜。非常整潔和可維護。

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

**代碼將不會編譯,我知道:)




使用DI的主要原因是你要把知識的實施知識的責任放在那裡。 DI的思想與接口的封裝和設計非常接近。 如果前端從後端詢問一些數據,那麼前端如何解決這個問題對於前端來說是不重要的。 這取決於請求處理者。

這在OOP中已經很常見了。 多次創建代碼片段,如:

I_Dosomething x = new Impl_Dosomething();

缺點是實現類仍然是硬編碼的,因此具有實現被使用的知識的前端。 DI進一步採用接口設計,前端唯一需要了解的是接口知識。 在DYI和DI之間是服務定位器的模式,因為前端必須提供一個鍵(存在於服務定位器的註冊表中)以使其請求得到解決。 服務定位器示例:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI例子:

I_Dosomething x = DIContainer.returnThat();

DI的要求之一是容器必須能夠找出哪個類是哪個接口的實現。 因此,DI容器需要強類型設計,並且每個接口同時只需要一個實現。 如果您需要更多的接口實現(如計算器),則需要服務定位器或工廠設計模式。

D(b)I:依賴注入和接口設計。 但這個限制並不是一個很大的實際問題。 使用D(b)I的好處是它可以為客戶和提供者之間的溝通提供服務。 接口是對像或一組行為的透視圖。 後者在這里至關重要。

我更喜歡與D(b)I一起編寫服務合同的管理。 他們應該一起去。 在我看來,使用D(b)I作為技術解決方案而沒有對服務合同進行組織管理,因為DI只是一個額外的封裝層,所以不是很有利。 但是,如果您可以將其與組織管理一起使用,則可以真正利用我提供的組織原則D(b)。 從長遠來看,它可以幫助您與客戶和其他技術部門就測試,版本控制和開發替代方案等主題進行溝通。 當你在硬編碼類中有一個隱式接口的時候,那麼當你使用D(b)I進行顯式定義的時候,它是否會隨著時間的推移而少得多地傳播。 這一切都歸結為維修,這是隨著時間的推移,而不是一次。 :-)