依赖注入与服务定位

44

我目前在权衡使用 DI 和 SL 的优缺点。然而,我发现自己陷入了以下两难困境:我应该仅将 IoC 容器注入到每个类中,然后对所有内容使用 SL。

DI 两难困境:

有些依赖项,例如 Log4Net 对于 DI 来说并不适用。我称这些为元依赖项,并认为它们对调用代码应该是不透明的。我的理由是,如果一个简单的类 'D' 最初没有实现日志记录,然后增长到需要日志记录,则依赖的类 'A'、'B'和'C' 必须从 'A' 传递它下来到 'D' (假设 'A' 包含 'B','B' 包含 'C',以此类推)。我们现在已经因为一个类需要记录日志而做出了重大的代码更改。

因此,我们需要一种获取元依赖项的不透明机制。两种方法浮现出来:单例和 SL。前者有已知的限制,主要是与刚性作用域相关:最好的单例将使用存储在应用程序范围(即静态变量中)的抽象工厂。这样可以提供一定的灵活性,但不完美。

一个更好的解决方案是将 IoC 容器注入到这些类中,然后从该类中使用 SL 从容器中解析这些元依赖项。

因此两难困境:由于现在正在向类中注入 IoC 容器,那么为什么不使用它来解析所有其他依赖项呢?

非常感谢您的想法 :)


5
你关于需要从A->B->C->D传递的评论似乎混淆了运行时和创建时间。请参见http://misko.hevery.com/2009/03/30/collaborator-vs-the-factory/comment-page-1/。 - WW.
1
太好了,WW的评论实际上帮助我纠正了我在依赖注入方面一直存在的障碍。 - Lawrence Wagerfield
在我看来,服务定位是穷人版的控制反转。它可以给你一些正确的IoC的好处,但由于不够明确,也会失去许多好处。依赖注入才是王道。 - kay.one
11个回答

63
由于该类现在正在使用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容器可以帮助解决此问题。在此处开始。


我完全同意后者是最清晰,最明确的。然而,您不认为像Log4Net这样的类不应该被注入吗? - Lawrence Wagerfield
5
@Lawrence Wagerfield:我不同意。请看一下我对处理日志的正确方式的修改。要么注入它们,要么使用AOP方法。 - jason
1
@Jason:我不会说 DI 的主要目的是使依赖关系显式,而是使它们可注入。大多数人认为构造函数/属性注入是无可置疑的好处,但并非所有人都这么认为。我认为原帖作者从实用的角度提出了一个有效的观点。 - Thiru
1
我认为在第四个例子中应该是 this.bar = bar; 而不是 this.Bar = bar; - BSeitkazin

6
服务定位器是一种反模式,详细原因可以参考http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx。在日志方面,您可以像处理其他依赖关系一样,通过构造函数或属性注入来注入抽象类。
唯一的区别是log4net需要使用服务的调用者类型。 使用Ninject(或其他容器),如何查找请求服务的类型?描述了如何解决这个问题(它使用Ninject,但适用于任何IoC容器)。
或者,您可以将日志记录视为横切关注点,不适合与业务逻辑代码混合使用,此时您可以使用拦截器,许多IoC容器都提供了此功能。 http://msdn.microsoft.com/en-us/library/ff647107.aspx介绍了如何在Unity中使用拦截器。

6
服务定位不是反模式。它只是一种经常被错误使用的模式,其目的是基于变量数据进行运行时解析。如果您在构造函数时间就可以访问信息,您可能不想直接使用定位器。 - Chris Marisic
针对基于变量数据的运行时解析,您可以使用工厂——手工制作(首选)或容器化。 - devdigital
工厂按定义只允许返回新实例,这消除了使用生命周期作用域对象的能力。一旦允许它返回任何生命周期作用域对象,它就成为了一个定位器。 - Chris Marisic
如果工厂包含对容器(定位器)的引用或者是一个容器工厂,则不需要这样做。无论哪种方式,都应该尽量限制使用定位器,并且它们仍然应被视为反模式。 - devdigital
我同意你想要限制它们的使用,但这绝对不是反模式。定位器的爆炸可能被认为是一种反模式,就像任何东西的爆炸一样是一种反模式。 - Chris Marisic
大家都链接那篇关于服务定位器是反模式的文章,但他们不知道何时适当地应用该模式。所给的例子是服务定位器模式的误用。服务定位器用于可插拔行为,其中许多具体实现实现相同的接口,并根据某些条件、配置或数据驱动决策选择实现。确实应该很少使用它们,但不是因为它是反模式,而是因为适用它的情况很少。像所有模式一样,不应盲目使用。 - AaronLS

5

我的观点是这取决于具体情况。有时候一种方式更好,有时候另一种方式更好。但是总的来说,我更喜欢依赖注入(DI)。以下是几个原因。

  1. 当某个依赖被注入到组件中时,它可以被视为其接口的一部分。因此,组件的用户更容易提供这些依赖项,因为它们是可见的。在注入 SL 或静态 SL 的情况下,这些依赖关系是隐藏的,使用组件会更加困难。

  2. 注入的依赖项更适合单元测试,因为您可以简单地模拟它们。在使用 SL 的情况下,您必须再次设置 Locator + 模拟依赖项。所以这需要更多的工作。


4
有时可以使用AOP实现日志记录,以使其不与业务逻辑混合。
否则,选项是:
  • 使用可选依赖项(例如setter属性),对于单元测试,您不会注入任何记录器。如果在生产环境中运行,则IOC容器将自动处理设置它。
  • 当你有一个依赖项,几乎每个应用程序对象都使用它(“记录器”对象是最常见的例子之一)时,这是少数情况下单例反模式变成良好实践之一。一些人称这些“好单例”为Ambient Contexthttp://aabs.wordpress.com/2007/12/31/the-ambient-context-design-pattern-in-net/
当然,这个上下文必须是可配置的,以便您可以在单元测试中使用存根/模拟。另一个建议使用AmbientContext的方法是将当前的日期/时间提供者放在那里,以便您可以在单元测试期间存根它,并加快时间。

2

这是关于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()方法实际上并没有遵循“控制反转”的原则。这也违反了方法级别的单一职责原则。为什么一个方法要关心通过new或任何S.L.类来实例化它需要完成任何事情的对象?
相反,构造函数可以实际上具有各自对象(读取依赖项)的参数,如下所示。那么服务定位器与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);

}

1

我知道这个问题有点老,但我想发表一下我的意见。

实际上,在大多数情况下,你真的不需要使用服务定位器(SL),而应该依赖于依赖注入(DI)。然而,有些情况下你应该使用SL。我发现自己在游戏开发中使用SL(或其变体)。

另一个SL的优点(在我看来)是能够传递内部类。

以下是一个例子:

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();
    }
}

正如你所看到的,由于我们没有进行依赖注入,而且即使我们能够进行依赖注入,该库的用户也不知道该方法被调用了。

1
我们达成了一个妥协:使用依赖注入,但将顶层依赖捆绑到一个对象中,避免如果这些依赖关系发生变化时引起的重构混乱。
在下面的示例中,我们可以添加到“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.
}

1

我知道人们认为DI是唯一好的IOC模式,但我不理解。我将尝试推销SL。我将使用新的MVC Core框架来向您展示我的意思。首先,DI引擎非常复杂。当人们说DI时,他们真正意思是使用像Unity、Ninject、Autofac这样的框架,这些框架会为您完成所有繁重的工作,而SL可以像制作一个工厂类那样简单。对于一个小而快速的项目,这是一种容易的方法来进行IOC,而无需学习整个框架以进行正确的DI,它们可能并不难学,但仍然需要时间。 现在来看DI可能会出现的问题。我将引用MVC Core文档中的一句话。 “ASP.NET Core从头开始设计,支持和利用依赖注入。”大多数人对DI有这样的说法,“你的代码库中99%的代码都不应该知道你的IoC容器。”那么如果只有1%的代码需要了解它,为什么他们需要从头开始设计呢?旧的MVC不支持DI吗?这就是DI的大问题,它取决于DI。使一切都“按照应该完成”的方式工作需要很多工作。如果您看看新的Action Injection,如果您使用[FromServices]属性,这不是依赖于DI吗?现在DI的人会说不,你应该使用工厂而不是这些东西,但正如您所看到的,甚至制作MVC的人也没有做对。Filters中的DI问题也很明显,请看看您需要做什么才能在filter中获取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()来完成此操作。然后我们来看看Views。尽管他们在DI方面有很好的意愿,但他们不得不在views中使用SL。新语法 @inject StatisticsService StatsService 相当于 var StatsService = Locator.Get()。DI最广告的部分是单元测试。但人们最终只是测试他们的模拟服务而没有目的,或者不得不连接他们的DI引擎进行真正的测试。我知道你可以做任何事情,但即使他们不知道它是什么,人们最终会制作一个SL定位器。并不是很多人在未阅读相关内容之前就开始使用DI。我对DI最大的问题是类的用户必须了解类的内部工作才能使用它。
SL可以以良好的方式使用,并具有一些优点,最重要的是其简单性。

1
我曾在Java中使用Google Guice DI框架,发现它不仅使测试更容易,而且可以实现更多功能。例如,我需要每个应用程序(而不是类)都有一个单独的日志记录器,并且所有常用库代码都使用当前调用上下文中的记录器。注入日志记录器使这成为可能。尽管所有库代码都需要进行更改:将记录器注入构造函数中。起初,我因所需的所有编码更改而对此方法持抵制态度;最终我意识到这些更改带来了许多好处:
  • 代码变得更简单
  • 代码变得更加健壮
  • 类的依赖关系变得明显
  • 如果有许多依赖项,则清楚地表明需要重构类
  • 静态单例被消除
  • 会话或上下文对象的需求消失
  • 多线程变得更容易,因为DI容器可以构建为仅包含一个线程,从而消除意外的交叉污染
毋庸置疑,我现在是DI的忠实拥护者,并将其用于除最琐碎的应用程序之外的所有应用程序。

0

如果示例只依赖于log4net,则您只需要执行以下操作:

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

将依赖注入到log4net中没有意义,因为它通过将类型(或字符串)作为参数来提供细粒度的日志记录。

此外,DI与SL无关。在我看来,ServiceLocator的目的是解决可选依赖项。

例如:如果SL提供ILog接口,我将编写日志数据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接