依赖注入与服务位置

Posted

技术标签:

【中文标题】依赖注入与服务位置【英文标题】:Dependency Injection vs Service Location 【发布时间】:2011-06-26 12:10:28 【问题描述】:

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

DI Catch 22:

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

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

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

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

非常感谢您的想法:)

【问题讨论】:

您关于需要将其从 A->B->C->D 传递下来的评论表明您混淆了运行时和创建时间。见misko.hevery.com/2009/03/30/collaborator-vs-the-factory/… 很棒的评论 WW,老实说,这有助于纠正我一直有重新 DI 的一个很大的心理障碍。 恕我直言,服务位置是穷人的控制反转,它为您提供了适当 IoC 的一些好处,但由于不明确,它也失去了许多好处。依赖注入是它的所在。 【参考方案1】:

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

【讨论】:

【参考方案2】:

我知道这个问题有点老了,我只是想我会给出我的意见。

实际上,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,反正我们也无法做到。

【讨论】:

【参考方案3】:

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

对象(通过新的或任何 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);


【讨论】:

【参考方案4】:

我知道人们确实在说 DI 是唯一好的 IOC 模式,但我不明白这一点。我会试着卖一点SL。我将使用新的 MVC Core 框架向您展示我的意思。第一个 DI 引擎非常复杂。当人们说 DI 时,他们真正的意思是使用 Unity、Ninject、Autofac 等框架来为您完成所有繁重的工作,而 SL 可以像制作工厂类一样简单。对于一个小型快速项目,这是一种无需学习适当 DI 的完整框架即可进行 IOC 的简单方法,它们可能并不难学习,但仍然如此。 现在到 DI 可能成为的问题。我将使用 MVC Core 文档中的引用。 “ASP.NET Core 的设计初衷就是支持和利用依赖注入。”大多数人说,关于 DI,“你的 99% 的代码库应该不知道你的 IoC 容器。”那么,如果只有 1% 的代码应该意识到这一点,那么为什么他们需要从头开始设计,旧的 MVC 不支持 DI 吗?好吧,这是 DI 的大问题,它取决于 DI。让每件事都“按照应该做的那样”工作需要做很多工作。如果您查看新的动作注入,如果您使用[FromServices] 属性,这是否取决于 DI。现在 DI 人会说不,你应该使用工厂而不是这些东西,但正如你所见,即使是制作 MVC 的人也没有做对。 DI 的问题在 Filters 中是可见的,看看你需要做什么才能在 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(); 来完成此操作。然后我们来到意见。出于对 DI 的所有善意,他们不得不使用 SL 来获取视图。新语法@inject StatisticsService StatsServicevar StatsService = Locator.Get&lt;StatisticsService&gt;(); 相同。 DI 中宣传最多的部分是单元测试。但是人们所做的只是在没有目的的情况下测试模拟服务,或者必须在那里连接 DI 引擎来进行真正的测试。而且我知道你可以做任何不好的事情,但人们最终会制作 SL 定位器,即使他们不知道它是什么。没有很多人在没有先阅读它的情况下制作 DI。 我对 DI 的最大问题是该类的用户必须了解其他类的内部工作原理才能使用它。 SL 可以很好地使用,并且有一些优点,最重要的是它的简单性。

【讨论】:

【参考方案5】:

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

在下面的示例中,我们可以添加到“ServiceDependencies”,而无需重构所有派生的依赖项。

示例:

public ServiceDependencies
     public ILogger Loggerget; private set;
     public ServiceDependencies(ILogger logger)
          this.Logger = logger;
     


public abstract class BaseService
     public ILogger Loggerget; 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】:

因为这个类现在被注入了一个 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。

【讨论】:

我完全同意后者是最干净和最明确的。但是,您不同意不应注入像 Log4Net 这样的类吗? @Lawrence Wagerfield:我不同意。请参阅我对处理日志记录的正确方式的编辑。要么注入它们,要么使用 AOP 方法。 @Jason:我不会说 DI 的 main 点是使依赖项显式化,而不是使它们可注入。大多数人认为 ctor/property 注入是一个毫无疑问的好处,但不是全部。我认为原始海报从务实的角度提出了一个有效的观点。 我认为在第四个例子中应该是this.bar = bar;而不是this.Bar = bar;【参考方案7】:

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

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

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

【讨论】:

【参考方案8】:

如果示例只以log4net作为依赖,那么你只需要这样做:

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

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

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

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

【讨论】:

【参考方案9】:

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

否则,选项是:

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

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

【讨论】:

【参考方案10】:

服务定位器是一种反模式,原因在 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 进行拦截。

【讨论】:

服务位置不是反模式。它只是一种经常使用不当的模式。目的是基于可变数据的运行时分辨率。如果您在构造函数时可以访问信息,您可能不想直接使用定位器。 对于基于可变数据的运行时分辨率,您拥有工厂 - 手工制作(首选)或基于容器。 根据定义,工厂只允许返回新实例,这消除了使用生命周期范围对象的能力,一旦你允许它返回任何生命周期范围的对象。它是一个定位器。 如果工厂包含对容器(定位器)的引用或者是容器工厂,则不会。无论哪种方式,都希望限制定位器的使用,并且它们仍应被视为反模式。 我同意你想限制它们的使用,但这绝对不是反模式。定位器的爆炸可以被认为是一种反模式,就像 anything 的爆炸是一种反模式一样。【参考方案11】:

我的看法是视情况而定。有时一个更好,有时另一个。但我会说一般来说我更喜欢 DI。有几个原因。

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

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

【讨论】:

以上是关于依赖注入与服务位置的主要内容,如果未能解决你的问题,请参考以下文章

最通俗易懂的依赖注入之服务注册与注入

Angular依赖注入机制与服务的作用范围

最通俗易懂的依赖注入与控制反转

[ASP.NET Core 3框架揭秘] 依赖注入:一个Mini版的依赖注入框架

ng2 学习笔记依赖注入与服务

ASP.NET Core 依赖注入最佳实践——提示与技巧