42

我目前正在权衡DI和SL之间的优缺点。但是,我发现自己处于以下 catch 22 中,这意味着我应该对所有事情都使用 SL,并且只将 IoC 容器注入每个类。

DI 第 22 条:

一些依赖项,例如 Log4Net,根本不适合 DI。我将这些元依赖称为元依赖,并认为它们对调用代码应该是不透明的。我的理由是,如果一个简单的类“D”最初是在没有日志记录的情况下实现的,然后增长到需要日志记录,那么依赖类“A”、“B”和“C”现在必须以某种方式获得这种依赖关系并将其从“A”到“D”(假设“A”组成“B”,“B”组成“C”,依此类推)。我们现在对代码进行了重大更改,只是因为我们需要登录一个类。

因此,我们需要一种不透明的机制来获得元依赖。我想到了两个:Singleton 和 SL。前者有已知的限制,主要是关于严格的作用域能力:单例最多将使用存储在应用程序范围内的抽象工厂(即在静态变量中)。这允许一些灵活性,但并不完美。

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

因此捕获 22:因为该类现在被注入一个 IoC 容器,那么为什么不使用它来解决所有其他依赖项呢?

我将非常感谢您的想法:)

4

11 回答 11

61

因为这个类现在被注入了一个 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 容器可以帮助解决这个问题。从这里开始。

于 2011-02-13T17:14:18.090 回答
7

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

与 log4net 的唯一区别是它需要使用服务的调用者的类型。 使用 Ninject(或其他容器)如何找出请求服务的类型?描述了如何解决这个问题(它使用 Ninject,但适用于任何 IoC 容器)。

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

于 2011-02-13T17:22:28.877 回答
5

我的看法是,这取决于。有时一个更好,有时另一个。但我会说一般来说我更喜欢 DI。原因很少。

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

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

于 2011-02-13T17:14:27.423 回答
4

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

否则,选项是:

  • 使用可选的依赖项(例如 setter 属性),并且对于单元测试,您不会注入任何记录器。如果您在生产环境中运行,IOC 容器会自动为您设置。
  • 当您的应用程序的几乎每个对象都在使用依赖项时(“记录器”对象是最常见的示例),这是单例反模式成为良好实践的少数情况之一。有些人称这些“好的单身人士”为环境上下文http ://aabs.wordpress.com/2007/12/31/the-ambient-context-design-pattern-in-net/

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

于 2012-04-12T11:50:59.877 回答
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() 方法实际上并不遵循“控制反转”原则。它还在方法级别打破了单一职责原则。为什么方法应该关注实例化

它需要完成任何事情的对象(通过新的或任何 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);

}
于 2017-06-01T09:13:10.487 回答
1

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

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

不用说,我现在是 DI 的忠实拥护者,除了最琐碎的应用程序之外,我都将它用于所有应用程序。

于 2013-05-08T17:35:18.687 回答
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,不是说我们无论如何都能做到。

于 2018-04-14T17:21:03.630 回答
1

我们达成了妥协:使用 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.
}
于 2016-03-18T13:47:11.310 回答
0

如果该示例仅将 log4net 作为依赖项,那么您只需要这样做:

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

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

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

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

于 2012-07-06T14:32:05.777 回答
0

我知道人们真的在说 DI 是唯一好的 IOC 模式,但我不明白这一点。我会试着卖一点SL。我将使用新的 MVC Core 框架向您展示我的意思。第一个 DI 引擎非常复杂。人们说 DI 的真正意思是使用 Unity、Ninject、Autofac 等框架,为您完成所有繁重的工作,其中 SL 可以像制作工厂类一样简单。对于一个小型快速项目,这是一种无需学习适当 DI 的完整框架即可进行 IOC 的简单方法,它们可能并不难学习,但仍然如此。现在到 DI 可能成为的问题。我将使用 MVC Core 文档中的引用。“ASP.NET Core 的设计初衷就是支持和利用依赖注入。” 大多数人说,关于 DI,“你的 99% 的代码库应该不知道你的 IoC 容器。[FromServices]属性。现在 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 可以很好地使用,并且有一些优点,最重要的是它的简单性。

于 2016-10-01T17:48:59.393 回答
0

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

于 2019-08-04T17:15:04.030 回答