簡體   English   中英

依賴注入與服務位置

[英]Dependency Injection vs Service Location

我目前正在權衡DI和SL之間的優缺點。 但是,我發現自己處於以下問題22中,這意味着我應該只使用SL作為一切,並且只在每個類中注入一個IoC容器。

DI Catch 22:

一些依賴項,如Log4Net,根本不適合DI。 我稱之為元依賴關系並認為它們對調用代碼應該是不透明的。 我的理由是,如果一個簡單的類'D'最初是在沒有記錄的情況下實現的,然后增長到需要記錄,那么依賴類'A','B'和'C'現在必須以某種方式獲得這種依賴並將其從'A'到'D'(假設'A'組成'B','B'組成'C',依此類推)。 我們現在已經進行了重要的代碼更改,因為我們需要登錄一個類。

因此,我們需要一種不透明的機制來獲取元依賴性。 我想到了兩個:Singleton和SL。 前者具有已知的局限性,主要是關於剛性范圍的能力:最好的是Singleton將使用存儲在應用程序范圍內的抽象工廠(即在靜態變量中)。 這允許一些靈活性,但並不完美。

更好的解決方案是將IoC容器注入此類,然后使用該類中的SL從容器中解析這些元依賴關系。

因此,捕獲22:因為類現在正在注入IoC容器,那么為什么不使用它來解析所有其他依賴項呢?

我非常感謝你的想法:)

因為該類現在正在注入IoC容器,那么為什么不使用它來解析所有其他依賴項呢?

使用服務定位器模式完全打敗了依賴注入的一個主要點。 依賴注入的關鍵是使依賴關系顯式化。 一旦你通過不在構造函數中使它們成為顯式參數來隱藏這些依賴項,就不再需要完全成熟的依賴注入了。

這些都是名為Foo的類的構造函數(設置為Johnny Cash歌曲的主題):

錯誤:

public Foo() {
    this.bar = new Bar();
}

錯誤:

public Foo() {
    this.bar = ServiceLocator.Resolve<Bar>();
}

錯誤:

public Foo(ServiceLocator locator) {
    this.bar = locator.Resolve<Bar>();
}

對:

public Foo(Bar bar) {
    this.bar = bar;
}

只有后者才能明確依賴Bar

對於日志記錄,有一種正確的方法可以實現它,而不會滲透到您的域代碼中(它不應該,但如果確實如此,則使用依賴注入時段)。 令人驚訝的是,IoC容器可以幫助解決這個問題。 這里開始。

服務定位器是一種反模式,其原因在http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx中有詳細描述。 在日志記錄方面,您可以像其他任何一樣將其視為依賴項,並通過構造函數或屬性注入注入抽象。

與log4net唯一的區別是它需要使用該服務的調用者類型。 使用Ninject(或其他一些容器)如何找出請求服務的類型? 描述了如何解決這個問題(它使用Ninject,但適用於任何IoC容器)。

或者,您可以將日志記錄視為交叉問題,這不適合與業務邏輯代碼混合使用,在這種情況下,您可以使用由許多IoC容器提供的攔截。 http://msdn.microsoft.com/en-us/library/ff647107.aspx描述了使用Unity進行攔截。

我的意見是,這取決於。 有時一個更好,有時另一個。 但我會說通常我更喜歡DI。 原因很少。

  1. 當以某種方式將依賴注入組件時,它可以被視為其接口的一部分。 因此,組件的用戶更容易提供這種依賴,因為它們是可見的。 在注入SL或靜態SL的情況下,隱藏了依賴關系並且組件的使用有點困難。

  2. 注入的依賴對於單元測試更好,因為您可以簡單地模擬它們。 在SL的情況下,您必須再次設置Locator + mock依賴項。 所以這是更多的工作。

有時可以使用AOP實現日志記錄,因此它不會與業務邏輯混合。

否則,選項是:

  • 使用可選的依賴項 (例如setter屬性),對於單元測試,不要注入任何記錄器。 如果您在生產中運行,IOC容器將自動為您設置它。
  • 當你有一個依賴項,幾乎你的應用程序的每個對象都在使用(“記錄器”對象是最常見的例子),這是單身反模式成為一個好習慣的少數情況之一。 有些人將這些“好單身人士”稱為環境背景http//aabs.wordpress.com/2007/12/31/the-ambient-context-design-pattern-in-net/

當然,這個上下文必須是可配置的,這樣你就可以使用stub / mock進行單元測試。 另一個建議使用AmbientContext的方法是將當前的日期/時間提供程序放在那里,以便您可以在單元測試期間將其存根,並根據需要加速時間。

我在Java中使用了Google Guice DI框架,並發現它不僅僅使測試變得更容易。 例如,我需要為每個應用程序 (不是類)單獨記錄日志,並進一步要求所有公共庫代碼在當前調用上下文中使用記錄器。 注入記錄器使這成為可能。 不可否認,所有庫代碼都需要更改:記錄器是在構造函數中注入的。 起初,由於所需的所有編碼更改,我拒絕這種方法; 最終我意識到這些變化有很多好處:

  • 代碼變得更簡單了
  • 代碼變得更加健壯
  • 類的依賴性變得明顯
  • 如果存在許多依賴關系,則清楚地表明類需要重構
  • 靜態單身人士被淘汰出局
  • 對會話或上下文對象的需求消失了
  • 多線程變得更容易,因為DI容器可以構建為僅包含一個螺紋,從而消除了無意的交叉污染

毋庸置疑,我現在是DI的忠實粉絲,除了最瑣碎的應用程序之外,它還可以用於所有應用程序。

我們已經達成了妥協:使用DI但是將頂級依賴關系捆綁到一個對象中,避免在這些依賴關系發生變化時重構地獄。

在下面的示例中,我們可以添加到“ServiceDependencies”,而無需重構所有派生的依賴項。

例:

public ServiceDependencies{
     public ILogger Logger{get; private set;}
     public ServiceDependencies(ILogger logger){
          this.Logger = logger;
     }
}

public abstract class BaseService{
     public ILogger Logger{get; private set;}

     public BaseService(ServiceDependencies dependencies){
          this.Logger = dependencies.Logger; //don't expose 'dependencies'
     }
}


public class DerivedService(ServiceDependencies dependencies,
                              ISomeOtherDependencyOnlyUsedByThisService                       additionalDependency) 
 : base(dependencies){
//set local dependencies here.
}

這是關於Mark Seeman的“服務定位器是一種反模式”。 我可能在這里錯了。 但我只是覺得我也應該分享我的想法。

public class OrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        var validator = Locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper = Locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}

OrderProcessor的Process()方法實際上並不遵循“控制反轉”原則。 它還打破了方法級別的單一責任原則。 為什么一個方法應該關注實例化

對象(通過新的或任何SL類)它需要完成任何事情。

而不是使用Process()方法創建對象,構造函數實際上可以具有相應對象的參數(讀取依賴項),如下所示。 那么服務定位器如何與IOC有任何不同

容器。 它也有助於單元測試。

public class OrderProcessor : IOrderProcessor
{
    public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
    {
        this.validator = validator; 
        this.shipper = shipper;
    }

    public void Process(Order order)
    {

        if (this.validator.Validate(order))
        {
            shipper.Ship(order);
        }
    }
}


//Caller
public static void main() //this can be a unit test code too.
{
var validator = Locator.Resolve<IOrderValidator>(); // similar to a IOC container 
var shipper = Locator.Resolve<IOrderShipper>();

var orderProcessor = new OrderProcessor(validator, shipper);
orderProcessor.Process(order);

}

我知道這個問題有點老了,我以為我會給出我的意見。

實際上,10次中有9次你真的不需要 SL並且應該依賴DI。 但是,在某些情況下您應該使用SL。 我發現自己使用SL(或其變體)的一個領域是游戲開發。

SL(在我看來)的另一個優點是能夠傳遞internal類。

以下是一個例子:

internal sealed class SomeClass : ISomeClass
{
    internal SomeClass()
    {
        // Add the service to the locator
        ServiceLocator.Instance.AddService<ISomeClass>(this);
    }

    // Maybe remove of service within finalizer or dispose method if needed.

    internal void SomeMethod()
    {
        Console.WriteLine("The user of my library doesn't know I'm doing this, let's keep it a secret");
    }
}

public sealed class SomeOtherClass
{
    private ISomeClass someClass;

    public SomeOtherClass()
    {
        // Get the service and call a method
        someClass = ServiceLocator.Instance.GetService<ISomeClass>();
        someClass.SomeMethod();
    }
}

正如你所看到的,庫的用戶不知道這個方法被調用了,因為我們沒有DI,不是我們無論如何都不能。

如果該示例僅將log4net作為依賴項,那么您只需要執行以下操作:

ILog log = LogManager.GetLogger(typeof(Foo));

沒有必要注入依賴關系,因為log4net通過將類型(或字符串)作為參數提供粒度日志記錄。

此外,DI與SL無關。 恕我直言,ServiceLocator的目的是解決可選的依賴項。

例如:如果SL提供ILog接口,我將編寫日志記錄daa。

我知道人們真的說DI是唯一的好IOC模式,但我不明白。 我會嘗試賣掉SL。 我將使用新的MVC Core框架向您展示我的意思。 第一台DI發動機非常復雜。 當人們說DI時,人們真正的意思是使用像Unity,Ninject,Autofac這樣的框架......為你做所有繁重的工作,SL可以像制造工廠一樣簡單。 對於一個小型的快速項目,這是一個簡單的方法來做IOC而不學習正確的DI的整個框架,他們可能不是那么難學,但仍然。 現在到了DI可以成為的問題。 我將使用MVC Core docs的引用。 “ASP.NET Core是從頭開始設計的,用於支持和利用依賴注入。” 大多數人都說DI“99%的代碼庫應該不了解你的IoC容器。” 那么為什么他們需要從頭開始設計,如果只有1%的代碼應該知道它,那么舊的MVC不支持DI嗎? 那么這是DI的重大問題,它取決於DI。 讓一切正常“因為它應該完成”需要做很多工作。 如果您使用[FromServices]屬性,如果查看新的Action Injection,則不依賴於DI。 現在DI人們會說不,你不應該選擇工廠而不是這個東西,但正如你所看到的那樣,即使是制造MVC的人也沒有做到這一點。 在過濾器中可以看到DI的問題,看看在過濾器中獲取DI需要做些什么

public class SampleActionFilterAttribute : TypeFilterAttribute
{
    public SampleActionFilterAttribute():base(typeof(SampleActionFilterImpl))
    {
    }

    private class SampleActionFilterImpl : IActionFilter
    {
        private readonly ILogger _logger;
        public SampleActionFilterImpl(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            _logger.LogInformation("Business action starting...");
            // perform some business logic work

        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            // perform some business logic work
            _logger.LogInformation("Business action completed.");
        }
    }
}

如果您使用SL,則可以使用var _logger = Locator.Get();來完成此操作。 然后我們來到觀點。 有了DI的良好意願,他們必須使用SL作為觀點。 新語法@inject StatisticsService StatsServicevar StatsService = Locator.Get<StatisticsService>(); DI中最廣告的部分是單元測試。 但是人們和正在做的只是測試模擬服務沒有任何目的或者必須在那里連接DI引擎來進行真正的測試。 而且我知道你可以做任何不好的事情,但即使他們不知道它是什么,人們也會最終制作SL定位器。 沒有很多人在沒有先閱讀的情況下制作DI。 DI的最大問題是類的用戶必須知道該類的內部工作方式才能使用它。
SL可以很好地使用,並且具有一些優點,最重要的是它的簡單性。

對於DI,您是否需要對注入型組件進行硬引用? 我沒有看到有人在談論這個問題。 對於SL,我可以告訴我的解析器在需要時從config.json或類似地動態加載我的類型。 此外,如果您的程序集包含數千種類型及其繼承,您是否需要對服務集合提供程序進行數千次級聯調用才能注冊它們? 這就是我看到很多話題的地方。 大多數人都在談論DI的好處及其一般情況,當談到如何在.net中實現它時,他們提出了一種擴展方法,用於添加對硬鏈接類型程序集的引用。 這對我來說並不是很脫鈎。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM