依赖注入和服务定位器模式有什么区别?

366

这两种模式似乎都是控制反转原则的一种实现方式。也就是说,一个对象不应该知道如何构造它的依赖。

依赖注入(DI)似乎使用构造函数或设置器来“注入”它的依赖关系。

Constructor Injection的示例:

//Foo Needs an IBar
public class Foo
{
  private IBar bar;

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

  //...
}

服务定位器似乎使用一个“容器”,它连接它的依赖项并为foo提供它的栏。

使用服务定位器的示例:

//Foo Needs an IBar
public class Foo
{
  private IBar bar;

  public Foo()
  {
    this.bar = Container.Get<IBar>();
  }

  //...
}
由于我们的依赖关系本身就是对象,这些依赖关系又有依赖关系,而这些依赖关系又有更多的依赖关系,依此类推。因此,控制反转容器(或 DI 容器)应运而生。例如:Castle Windsor、Ninject、Structure Map、Spring 等。
但是,IOC/DI容器看起来与服务定位器完全相同。将其称为DI容器是一个不好的命名方式吗?IOC/DI容器只是另一种类型的服务定位器吗?微妙之处在于我们通常在具有许多依赖项时使用DI容器。

16
控制反转意味着“一个对象不应该知道如何构造它的依赖项”?!这个对我来说是新的。不,真的,这不是“控制反转”的意思。请参见http://martinfowler.com/bliki/InversionOfControl.html。该文章甚至提供了有关术语源头的参考资料,追溯到20世纪80年代。 - Rogério
1
依赖注入成功之路 - Mark Seemann
4
Mark Seemann认为服务定位器是一种反模式(http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/)。我发现这张图(在这里找到,https://dev59.com/oWkx5IYBdhLWcg3wAvqn#9503612)有助于理解依赖注入和服务定位器的困境。希望这可以帮到您。 - VivekDev
谢谢,我会看一下。我记得单例模式是反面模式,但是大家都在使用它们。 - Zippy
16个回答

239

看起来这两者的区别可能微不足道,但即使使用了ServiceLocator,类仍然负责创建其依赖项。它只是使用服务定位器来完成这个任务。而使用DI,类会被提供它所需要的依赖项,它不关心依赖项来自哪里。其中一个重要的结果是DI示例更容易进行单元测试--因为可以传递模拟实现它所依赖的对象。如果您愿意,可以将两者结合起来并注入服务定位器(或工厂)。


27
此外,在构建类时,您可以同时使用两者。默认构造函数可以使用SL检索依赖项,并将它们传递给接收这些依赖项的“真实”构造函数。您可以得到最好的双重优势。 - Grant Palin
8
不,ServiceLocator 负责为给定的依赖项(插件)实例化正确的实现。在依赖注入的情况下,依赖注入“容器”负责这个过程。 - Rogério
6
@Rogerio是的,但是这个类仍然必须知道服务定位器...这是两个依赖项。此外,我经常看到服务定位器委托给DI容器进行查找,特别是对于需要服务支持的瞬态对象。 - Adam Gent
4
重要结果之一是,DI示例更容易进行单元测试--因为您可以将对象的模拟实现传递给它所依赖的对象。但这并不正确。在您的单元测试中,可以调用服务定位器容器中的注册函数,以轻松地将模拟添加到注册表中。 - Drumbeg
2
@Drumbeg - 好的,现在将其更改为使用不同的定位器或更改定位器如何进行注册。看到了吗?这就是服务定位器的问题 - 您已将实现和测试与实际工作类不需要的东西(仅配置)耦合在一起。 - tvanfosson
显示剩余9条评论

117
当您使用服务定位器时,每个类都将依赖于您的服务定位器。而这种情况在依赖注入中并不会发生。依赖注入通常仅在启动时调用一次以向某个主类注入依赖项。该主类所依赖的类将递归地注入其依赖项,直到您获得一个完整的对象图。
一个很好的比较:http://martinfowler.com/articles/injection.html 如果您的依赖注入器看起来像一个服务定位器,其中类直接调用注入器,则它可能不是依赖注入器,而是服务定位器。

21
但是当你需要在运行时创建对象时,该如何处理呢?如果你用“new”手动创建对象,你就不能使用 DI。如果你调用 DI 框架来帮助你,那么就破坏了这个模式。那还有哪些选择呢? - Boris
11
我遇到了同样的问题,所以决定注入特定类别的工厂。虽然不太美观,但完成了工作。希望能看到更漂亮的解决方案。 - Charlie Rudenstål
比较的直接链接:http://martinfowler.com/articles/injection.html#ServiceLocatorVsDependencyInjection - Teoman shipahi
2
@Boris 如果我需要动态构建新对象,我会注入一个用于创建这些对象的抽象工厂。这类似于在此情况下注入服务定位器,但提供了一个具体、统一、编译时的接口来构建相关对象,并使依赖关系明确化。 - NotVeryMoe

59

服务定位器可以隐藏依赖关系,在使用定位器获取连接时,无法从对象上看出它是否会访问数据库等。而使用依赖注入(至少是构造函数注入),依赖关系就是明确的。

此外,服务定位器破坏了封装性,因为它们为其他对象的依赖项提供了全局访问点。使用服务定位器,就像任何单例一样

很难指定客户端对象接口的前后置条件,因为其实现的运作方式可能会被外部干扰。

使用依赖注入后,一旦指定了对象的依赖关系,它们就在对象自己的控制之下。


3
我更喜欢“单例被认为很蠢”,http://steve.yegge.googlepages.com/singleton-considered-stupid - Charles Graham
2
我喜欢老史蒂夫·叶吉(Steve Yegge),他的文章标题很棒,但是我认为我引用的文章和米什科·赫维(Miško Hevery)的“单例模式是病态的谎言”(http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/)更好地阐述了服务定位器这一特定魔鬼行为的缺陷。 - Jeff Sternal
这个答案是最正确的,因为它最好地定义了服务定位器:“隐藏其依赖关系的类。”请注意,虽然在内部创建依赖关系通常不是一件好事,但这并不意味着一个类就成为了服务定位器。此外,对容器进行依赖是一个问题,但并不是最清晰地定义服务定位器的“问题”。 - user3230660
1
使用依赖注入(至少是构造函数注入),依赖关系是显式的。这意味着,每个类都明确声明了它所依赖的其他类或对象,而不是在类内部创建这些依赖关系。这种方法使得代码更加模块化和可测试,因为每个类都可以单独测试,并且容易替换其依赖项。 - FindOutIslamNow
1
与上述相同,我无法看出SL如何使依赖性比DI更不明确... - Michał Powłoka
服务定位器不必是单例。此外,您可以将它们设计为抛出编译时错误,而不是使用 DI 时会遇到的运行时错误。以下是我的用法:https://github.com/cowwoc/pouch - Gili

57

马丁·福勒(Martin Fowler)认为:

使用服务定位器,应用程序类通过向定位器发送消息显式地请求它。使用依赖注入,没有显式的请求,服务出现在应用程序类中 - 因此是控制反转。

简而言之:服务定位器和依赖注入只是依赖倒置原则的实现方式。

重要的原则是“依赖于抽象,而不是具体物件”。这将使您的软件设计“松散耦合”,“可扩展”,“灵活”。

您可以使用最适合您需求的方法。对于大型应用程序,使用一种庞大的代码库,最好使用服务定位器,因为使用依赖注入需要更多的代码库更改。

您可以查看此文章:依赖倒置:服务定位器还是依赖注入

以及经典文章:Martin Fowler的IoC容器和依赖注入模式

Ralph E. Johnson和Brian Foote的可重用类的设计

然而,让我大开眼界的是:Dino Esposito的ASP.NET MVC:解决或注入?这是问题...


精彩摘要:“服务定位器和依赖注入只是依赖反转原则的实现。” - Hans
1
他还说: 关键区别在于,使用服务定位器的每个服务用户都依赖于定位器。定位器可以隐藏对其他实现的依赖,但您确实需要看到定位器。因此,在定位器和注入器之间做出决策取决于该依赖关系是否成问题。 - programaths
1
ServiceLocator和DI与“依赖倒置原则”(DIP)无关。 DIP是一种通过将高级组件对低级组件的编译时依赖替换为对与高级组件一起定义的抽象类型的依赖来使高级组件更可重用的方法,该抽象类型由低级组件实现; 通过这种方式,编译时依赖性被反转,因为现在是低级别的依赖于高级别的依赖。此外,请注意Martin Fowler的文章解释了DI和IoC不是相同的东西。 - Rogério

28

使用构造函数依赖注入的类表明存在需要被满足的依赖关系。如果该类在内部使用服务定位器(SL)检索这些依赖关系,那么消费代码将不会意识到这些依赖关系。表面上看起来可能更好,但实际上了解任何显式依赖关系是有益处的,从架构的角度来看更好。而且,在进行测试时,您必须知道一个类是否需要某些依赖关系,并配置SL以提供这些依赖关系的适当虚假版本。使用DI,只需传递虚假的依赖关系即可。虽然差异不大,但确实存在。

不过,DI和SL可以一起使用。拥有一个通用依赖项的中心位置(例如设置、日志记录器等)非常有用。对于使用此类依赖项的类,您可以创建一个“真实”的构造函数来接收依赖项,并创建一个默认(无参数)构造函数,它检索SL并转发给“真实”构造函数。

编辑:当您使用SL时,您正在引入与该组件的耦合。这是具有讽刺意味的,因为这种功能的想法是鼓励抽象并减少耦合。这些问题可以平衡,这取决于您需要在多少位置使用SL。如果按上述建议仅在默认类构造函数中使用,则可以平衡这些问题。


有趣!我同时使用 DI 和 SL,但不是用两个构造函数。三到四个最无聊但经常需要的依赖项(设置等)会在运行时从 SL 中获取。其他所有东西都是通过构造函数注入的。这有点丑陋,但很实用。 - maaartinus

19

它们都是IoC的实现技术。还有其他实现控制反转的模式:

  • 工厂模式
  • 服务定位器
  • DI(IoC)容器
  • 依赖注入(构造函数注入、参数注入(如果不需要)、setter注入或接口注入)...

服务定位器和DI容器看起来更相似,它们都使用容器来定义依赖关系,将抽象映射到具体的实现。

主要区别在于依赖项的定位方式,在服务定位器中,客户端代码请求依赖项,在DI容器中,我们使用容器创建所有对象,并将依赖项作为构造函数参数(或属性)进行注入。


依赖注入并不需要使用 DI 容器。在 DI 中,一个不那么常见的方法是使用纯 DI,这基本上意味着您在组合根中手动连接对象图。 - Steven

8

有一个添加的原因,受到上周为MEF项目撰写的文档更新启发(我帮助构建MEF)。

一旦应用程序由潜在的成千上万个组件组成,确定任何特定组件是否可以正确实例化就可能很困难。通过“正确实例化”,我指的是在以Foo组件为基础的这个示例中,将可用IBar实例,并且提供它的组件将具备:

  • 必需的依赖项,
  • 不涉及任何无效的依赖关系循环,并且
  • 在MEF的情况下,只被提供一次实例。

在你提供的第二个示例中,构造函数去IoC容器检索其依赖项,你能测试Foo的一个实例是否能够使用你的应用程序的实际运行时配置来正确实例化的唯一方法是实际构造它

这会在测试时间引起各种尴尬的副作用,因为在运行时正常工作的代码不一定在测试环境下有效。模拟对象不能解决问题,因为我们需要测试的是真实配置,而不是一些测试时间的设置。

这个问题的根源就是@Jon已经提到的差异:通过构造函数注入依赖项是声明式的,而第二个版本使用了命令式的服务定位器模式。

当小心使用时,IoC容器可以在不实际创建组件实例的情况下静态分析应用程序的运行时配置。许多流行的容器都提供了一些变体;面向.NET 4.5 Web和Metro样式应用程序的MEF版本Microsoft.Composition在维基文档中提供了一个CompositionAssert示例。使用它,你可以编写如下代码:

 // Whatever you use at runtime to configure the container
var container = CreateContainer();

CompositionAssert.CanExportSingle<Foo>(container);

(请参见此示例。)

通过在测试时间验证应用程序的组合根,您可以潜在地捕获一些在后续测试过程中可能会滑过的错误。

希望这是对该主题的全面回答的有趣补充!


8
在我的上一个项目中,我同时使用了依赖注入和服务定位器。我使用依赖注入来实现单元测试的可测试性。我使用服务定位器来隐藏实现并依赖于我的IoC容器。是的!一旦你使用了IoC容器(Unity、Ninject、Windsor Castle),你就会依赖它。一旦它过时了或者由于某些原因你想要替换它,你将需要更改你的实现——至少是组合根。但是服务定位器抽象了这个阶段。
你如何不依赖于你的IoC容器?要么你需要自己包装它(这是一个坏主意),要么你使用服务定位器来配置你的IoC容器。所以你会告诉服务定位器获取你需要的接口,它会调用已配置为检索该接口的IoC容器。
在我的情况下,我使用一个框架组件ServiceLocator。而我的IoC容器则是Unity。如果将来我需要将IoC容器替换为Ninject,我只需配置我的服务定位器以使用Ninject而不是Unity。轻松迁移。
这是一篇很棒的文章,解释了这种情况; http://www.johandekoning.nl/index.php/2013/03/03/dont-wrap-your-ioc-container/

1
johandekoning的文章链接已经失效。 - JakeJ

6

我认为这两者是相辅相成的。

依赖注入是指将某个依赖类/接口推送到使用该类的另一个类中(通常是通过构造函数)。这样通过接口进行解耦,意味着使用该类的另一个类可以与许多类型的“注入依赖”实现一起工作。

服务定位器的作用是将你的实现集合在一起。在程序启动时,你可以通过一些引导程序设置服务定位器。引导程序是将某个实现类型与特定abstract/interface相关联的过程。这些在运行时会根据你的配置或引导程序自动生成。(如果你没有实现依赖注入,那么很难利用服务定位器或IOC容器)。


5
注意:我并不是在回答这个问题。但我认为这对于那些对服务定位器(反)模式感到困惑的依赖注入模式的新学习者可能会有用,他们偶然间发现了这个页面。
我知道服务定位器(似乎现在被认为是反模式)和依赖注入模式之间的区别,并且可以理解每种模式的具体示例,但我对在构造函数中显示服务定位器的示例感到困惑(假设我们正在进行构造函数注入)。
“服务定位器”通常既用作一种模式的名称,也用作用于该模式中的对象(也假设如此)来获取对象而不使用new运算符的名称。现在,同样类型的对象也可以用于组成根以执行依赖注入,这就是混淆的原因所在。
需要翻译的内容:

需要注意的是,在DI构造函数中可能会使用服务定位器对象,但您并未使用“服务定位器模式”。如果将其称为IoC容器对象,则会更清楚,因为您可能已经猜到它们本质上执行相同的操作(如果我错了,请纠正我)。

无论是将其称为服务定位器(或仅定位器),还是称为IoC容器(或仅容器),都没有区别,因为您已经猜到,它们可能是指相同的抽象(如果我错了,请纠正我)。只是称之为服务定位器会暗示一个人正在使用服务定位器反模式与依赖注入模式一起使用。

在我看来,将其命名为“定位器”而不是“位置”或“定位”也会导致有时会认为文章中的服务定位器是指服务定位器容器,而不是服务定位器(反)模式,尤其是当存在称为依赖注入而不是依赖注入器的相关模式时。


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