依赖注入与服务位置

时间:2011-02-13 16:48:56

标签: c# dependency-injection singleton dependency-management service-locator

我目前正在权衡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容器,那么为什么不使用它来解析所有其他依赖项呢?

我非常感谢你的想法:)

11 个答案:

答案 0 :(得分:58)

  

因为该类现在正在注入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容器可以帮助解决这个问题。开始here

答案 1 :(得分:8)

服务定位器是一种反模式,出于http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx中出色的原因。在日志记录方面,您可以像其他任何一样将其视为依赖项,并通过构造函数或属性注入注入抽象。

与log4net的唯一区别在于它需要使用该服务的调用者类型。 Using Ninject (or some other container) How can I find out the type that is requesting the service?描述了如何解决这个问题(它使用Ninject,但适用于任何IoC容器)。

或者,您可以将日志记录视为一个跨领域问题,这不适合与业务逻辑代码混合使用,在这种情况下,您可以使用由许多IoC容器提供的拦截。 http://msdn.microsoft.com/en-us/library/ff647107.aspx描述了使用Unity进行拦截。

答案 2 :(得分:5)

我的意见是,这取决于。有时一个更好,有时另一个。但我会说通常我更喜欢DI。原因很少。

  1. 当以某种方式将依赖注入组件时,可以将其视为其接口的一部分。因此,组件的用户更容易提供这种依赖,因为它们是可见的。如果注入SL或静态SL,隐藏了依赖关系并且组件的使用有点困难。

  2. 对于单元测试,注入的依赖性更好,因为您可以简单地模拟它们。在SL的情况下,您必须再次设置Locator + mock依赖项。所以这是更多的工作。

答案 3 :(得分:4)

有时可以使用 AOP 实现日志记录,因此它不会与业务逻辑混合。

否则,选项为:

  • 使用可选依赖项(例如setter属性),对于单元测试,您不会注入任何记录器。如果您在生产中运行,IOC容器将自动为您设置它。
  • 如果你有一个依赖项,你的应用程序几乎每个对象都在使用(“logger”对象是最常见的例子),那么这是少数单例反模式成为一种好习惯的情况之一。有些人将这些“好单身人士”称为 Ambient Context http://aabs.wordpress.com/2007/12/31/the-ambient-context-design-pattern-in-net/

当然,这个上下文必须是可配置的,这样你就可以使用stub / mock进行单元测试。 另一个建议使用AmbientContext的方法是将当前的日期/时间提供程序放在那里,以便您可以在单元测试期间将其存根,并在需要时加快时间。

答案 4 :(得分:1)

我在Java中使用了Google Guice DI框架,并发现它不仅仅使测试变得更容易。例如,我需要为每个应用程序(不是类)单独记录日志,并进一步要求所有公共库代码在当前调用上下文中使用记录器。注入记录器使这成为可能。不可否认,所有库代码都需要更改:记录器是在构造函数中注入的。起初,由于所需的所有编码更改,我拒绝这种方法;最终我意识到这些变化有很多好处:

  • 代码变得更简单
  • 代码变得更加健壮
  • 一个类的依赖关系变得明显
  • 如果存在许多依赖关系,则清楚地表明某个类需要重构
  • 消除了静态单身
  • 对会话或上下文对象的需求消失
  • 多线程变得更容易,因为DI容器可以构建为仅包含一个螺纹,从而消除无意的交叉污染

毋庸置疑,我现在是DI的忠实粉丝,除了最琐碎的应用程序之外,我还可以使用它。

答案 5 :(得分:1)

我们已达成妥协:使用DI但将顶级依赖项捆绑到一个对象中,避免重构地狱,如果这些依赖项发生变化。

在下面的示例中,我们可以添加&#39; ServiceDependencies&#39;无需重构所有派生的依赖项。

示例:

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.
}

答案 6 :(得分:1)

这是关于&#39;服务定位器是反模式&#39;作者: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()方法实际上并没有遵循&#39; Inversion of Control&#39;原理。它还打破了方法级别的单一责任原则。为什么一个方法应该关注实例化

对象(通过新的或任何S.L.类)它需要完成任何事情。

而不是让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);

}

答案 7 :(得分:1)

我知道这个问题有点老了,我以为我会提出意见。

实际上,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,而不是我们能够做到的。

答案 8 :(得分:0)

如果示例仅将log4net作为依赖项,那么您只需要执行此操作:

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

没有必要注入依赖项,因为log4net通过将类型(或字符串)作为参数提供粒度日志记录。

此外,DI与SL无关。恕我直言,ServiceLocator的目的是解决可选的依赖关系。

例如:如果SL提供了ILog接口,我将编写日志记录daa。

答案 9 :(得分:0)

我知道人们真的说DI是唯一优秀的IOC模式,但我不明白。我会尝试卖掉SL。我将使用新的MVC Core框架向您展示我的意思。第一台DI发动机非常复杂。当人们说DI时,人们真正的意思是使用像Unity,Ninject,Autofac这样的框架......为你做所有繁重的工作,SL可以像制造工厂一样简单。对于一个小型的快速项目,这是一个简单的方法来做IOC而不学习正确的DI的整个框架,他们可能不是那么难学,但仍然。 现在到了DI可以成为的问题。我将使用MVC Core docs的引用。 &#34; ASP.NET核心是从头开始设计的,用于支持和利用依赖注入。&#34;大多数人都说关于DI&#34; 99%的代码库应该不了解你的IoC容器。&#34;那么,为什么他们需要从头开始设计,如果只有1%的代码应该知道它,那么老的MVC支持DI?那么这是DI的重大问题,它取决于DI。使一切工作&#34;因为它应该完成&#34;需要做很多工作。如果您使用[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可以很好地使用,并且具有一些优点,最重要的是它的简单性。

答案 10 :(得分:0)

对于DI,您需要对注入式装配有硬性参考吗?我看不到有人在谈论这个。对于SL,我可以告诉解析器何时需要从config.json或类似文件中动态加载我的类型。另外,如果程序集包含数千个类型及其继承,那么是否需要对服务收集提供者进行数千级联调用以注册它们?那是我确实谈论很多的地方。大多数人都在谈论DI的好处以及它的一般含义,当涉及到如何在.net中实现DI时,他们提出了一种扩展方法,用于添加对硬链接类型程序集的引用。这对我来说不是很重要。