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


303

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

依赖注入(DI)似乎使用构造函数或setter来“注入”它的依赖项。

使用构造函数注入的示例:

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

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

  //...
}

Service Locator似乎使用了一个“容器”,它连接了它的依赖项并给foo它的标志。

使用服务定位器的示例:

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

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

  //...
}

因为我们的依赖关系只是对象本身,所以这些依赖关系具有依赖关系,后者甚至具有更多的依赖关系,依此类推。因此,控制容器(或DI容器)的反转诞生了。示例:温莎城堡,Ninject,结构图,弹簧等)

但是,IOC / DI容器看起来完全相同像一个服务定位器。称它为DI容器是一个坏名字吗?IOC / DI容器仅仅是服务定位器的另一种类型吗?当我们有很多依赖项时,我们主要使用DI容器是否存在细微差别?


13
控制反转意味着“对象不应该知道如何构造其依赖项”?!?那对我来说是新的。不,真的,那不是“控制权倒置”的含义。参见martinfowler.com/bliki/InversionOfControl.html该文章甚至提供了该词源的参考,其历史可以追溯到1980年代。
罗杰里奥(Rogério)2010年


1
马克· 西曼(Mark Seemann)认为服务定位器是反模式(blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern)。另外,我发现该图(在此处找到 stackoverflow.com/a/9503612/1977871)有助于理解DI和SL的困境。希望这可以帮助。
VivekDev

Answers:


180

差异似乎很小,但是即使使用ServiceLocator,该类仍然负责创建其依赖项。它只是使用服务定位器来做到这一点。使用DI,该类将获得其依赖关系。它既不知道,也不在乎它们的来源。一个重要的结果是,DI示例更容易进行单元测试-因为您可以将其依赖对象的模拟实现传递给它。您可以将两者结合起来,并根据需要注入服务定位器(或工厂)。


20
此外,在构造类时,您可以同时使用两者。默认的构造函数可以使用SL检索依赖关系,并将它们传递给接收这些依赖关系的“真实”构造函数。您将两全其美。
格兰特·帕林

6
不,ServiceLocator是负责为给定依赖关系(插件)实例化正确实现的工具。在DI的情况下,DI“容器”是对此负责的人。
罗杰里奥(Rogério)2010年

5
@Rogerio是的,但是该类仍然必须了解服务定位器...多数民众赞成在两种依赖关系。我也经常看到服务定位器委托DI容器进行查找,尤其是对于需要服务支持的临时对象。
亚当·根特

2
@Adam我不是说服务定位器将委托给DI容器。如“官方”文章中所述,这是两种互斥的模式。在我看来,Service Locator在实践中比DI有一个巨大的优势:使用DI容器会引起滥用(这是我反复看到的),而使用Service Locator则不会。
罗杰里奥

3
“这样做的一个重要结果是,DI示例更易于进行单元测试-因为您可以将其依赖对象的模拟实现传递给它。” 不对。在单元测试中,可以使用对服务定位器容器中的register函数的调用来轻松将模拟添加到注册表中。
Drumbeg 2014年

92

当您使用服务定位器时,每个类都将依赖于您的服务定位器。依赖注入不是这种情况。依赖注入器通常在启动时仅被调用一次,以将依赖注入到某个主类中。该主类依赖的类将递归地注入其依赖项,直到您具有完整的对象图。

很好的比较:http : //martinfowler.com/articles/injection.html

如果您的依赖项注入器看起来像服务定位器,类直接在其中调用该注入器,则它可能不是依赖项注入器,而是服务定位器。


17
但是,如何处理在运行时必须创建对象的情况?如果使用“ new”手动创建它们,则不能使用DI。如果您致电DI框架寻求帮助,那将打破格局。那么还剩下什么选择呢?
鲍里斯(Boris)2013年

9
@鲍里斯(Boris)我遇到了同样的问题,因此决定注入特定班级的工厂。不漂亮,但是完成了工作。希望看到一个更漂亮的解决方案。
查理·鲁登斯塔尔2014年


2
@Boris如果我需要即时构造新对象,我会为所述对象注入一个Abstract Factory。在这种情况下,这类似于注入服务定位器,但提供了一个具体,统一,编译时的接口,用于构建相关对象并使相关性明确。
LivePastTheEnd

51

服务定位器隐藏依赖项-当对象从定位器获得连接时,您无法通过查看对象来判断它是否命中数据库(例如)。使用依赖项注入(至少是构造函数注入),显式依赖项就可以了。

而且,服务定位器破坏了封装,因为它们提供了对其他对象的依赖项的全局访问点。与服务定位器一样,与任何单例一样

很难为客户端对象的接口指定前后条件,因为可以从外部干预其实现的工作方式。

使用依赖项注入,一旦指定了对象的依赖项,它们就将在对象本身的控制下。


3
我更喜欢“ Singletons被认为是愚蠢的”,steve.yegge.googlepages.com / singleton
Charles Graham

2
我喜欢史蒂夫·叶格(Steve Yegge),而且这篇文章的标题很棒,但我认为我引用的文章以及MiškoHevery的“单句是病态骗子”(misko.hevery.com/2008/08/17/singletons-are-pathologic-骗子)针对服务定位器的特定缺陷提出了更好的理由。
杰夫·斯坦恩

这个答案是最正确的,因为它最好地定义了服务定位符:“隐藏其依赖关系的类”。请注意,在内部创建依赖项(虽然通常不是一件好事)并不能使类成为服务定位器。同样,依赖容器是一个问题,但不是最明确定义服务定位符的“那个”问题。
山姆

1
With dependency injection (at least constructor injection) the dependencies are explicit.。请解释。
FindOutIslamNow18年

如上所述,我看不到SL如何让依赖数量比DI不太明显的...
米哈尔Powłoka

38

马丁·福勒指出

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

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

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

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

您可以查看此帖子:依赖倒置:服务定位器或依赖注入

也是经典之作:Martin Fowler的控制容器反转和依赖注入模式

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

但是,让我大开眼界的是:ASP.NET MVC:是解析还是注入?这就是问题……Dino Esposito撰写


精彩摘要:“服务定位器和依赖注入只是依赖倒置原则的实现。”
Hans

他还指出:关键区别在于,使用服务定位器时,服务的每个用户都依赖于该定位器。定位器可以隐藏对其他实现的依赖关系,但是您确实需要查看定位器。因此,定位器和注入器之间的决定取决于该依赖关系是否成问题。
programaths

1
ServiceLocator和DI与“依赖关系反转原理”(DIP)没有关系。DIP是一种使高层组件更可重用的方法,方法是将对底层组件的编译时依赖性替换为与高层组件一起定义的抽象类型的依赖关系,该抽象类型由底层组件实现。级别组件;这样,编译时的依赖关系就被颠倒了,因为现在是低级别的依赖于高层的了。另外,请注意,Martin Fowler的文章解释了DI和IoC 不是同一件事。
罗杰里奥

23

使用构造函数DI的类指示使用代码存在要满足的依赖关系。如果该类在内部使用SL来检索此类依赖项,那么使用方代码将不了解这些依赖项。从表面上看,这似乎更好,但是了解任何显式依赖关系实际上是有帮助的。从体系结构的角度来看更好。并且在进行测试时,您必须知道一个类是否需要某些依赖项,并配置SL以提供这些依赖项的适当伪造版本。使用DI,只需传递假货即可。差别不大,但确实存在。

DI和SL可以一起工作。对于常见的依赖项(例如设置,记录器等),将其放置在中央位置非常有用。给定使用此类deps的类,您可以创建一个接收deps的“真实”构造函数,以及一个从SL检索并转发到“ real”构造函数的默认(无参数)构造函数。

编辑:并且,当然,当您使用SL时,您正在向该组件引入一些耦合。具有讽刺意味的是,因为这种功能的想法是鼓励抽象并减少耦合。这些问题可以得到平衡,这取决于您需要使用SL的地方。如果按照上面的建议进行操作,则只需在默认的类构造函数中即可。


有趣!我同时使用DI和SL,但没有两个构造函数。从SL即时获取三个或四个最无聊的经常需要的依赖项(设置等)。其他所有内容都注入了构造函数。这有点丑陋,但很实用。
maaartinus

10

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

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

服务定位器和DI容器似乎更相似,它们都使用容器来定义依赖项,从而将抽象映射到具体实现。

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


7

在上一个项目中,我同时使用了两者。我使用依赖注入来实现单元可测试性。我使用服务定位器来隐藏实现并依赖于我的IoC容器。是的!一旦使用了IoC容器之一(Unity,Ninject,Windsor Castle),您就可以依靠它。并且一旦过时或出于某种原因(如果您想交换它),您将/可能需要更改您的实现-至少是组合根。但是服务定位器可以抽象该阶段。

您如何不依赖IoC容器?您将需要自己包装它(这是一个坏主意),或者您可以使用Service Locator配置IoC容器。因此,您将告诉服务定位器获取所需的接口,并且它将调用配置为检索该接口的IoC容器。

就我而言,我使用的是ServiceLocator,它是一个框架组件。并使用Unity for IoC容器。如果将来我需要将IoC容器换成Ninject我需要做的就是配置服务定位器以使用Ninject而不是Unity。易于迁移。

这是一篇很棒的文章,解释了这种情况; http://www.johandekoning.nl/index.php/2013/03/03/dont-wrap-your-ioc-container/


johandekoning文章链接已断开。
JakeJ

6

我认为两者是一起工作的。

依赖注入意味着您将某个依赖类/接口推入一个消费类(通常是它的构造函数)。这通过接口将两个类解耦,这意味着使用类可以与许多类型的“注入依赖项”实现一起使用。

服务定位器的作用是将您的实现整合在一起。您可以在程序开始时通过一些引导绑定来设置服务定位器。引导程序是将一种实现类型与特定的抽象/接口相关联的过程。在运行时为您创建的。(基于您的配置或引导程序)。如果您尚未实现依赖注入,那么利用服务定位器或IOC容器将非常困难。


6

出于上周我们为MEF项目编写的文档更新的启发(我帮助构建MEF),添加了一个理由。

一旦应用程序由潜在的数千个组件组成,就很难确定是否可以正确实例化任何特定的组件。“正确实例化”是指在此示例中,该Foo组件基于组件,并且的实例IBar将可用,并且提供该组件的组件将:

  • 有必要的依赖关系
  • 不参与任何无效的依赖周期,并且
  • 对于MEF,仅提供一个实例。

在您给出的第二个示例中,构造函数转到IoC容器以获取其依赖关系,测试应用程序的实际运行时配置Foo能够正确实例化其实例的唯一方法是实际构造它

这在测试时会产生各种尴尬的副作用,因为可以在运行时运行的代码不一定会在测试工具下运行。嘲弄不会,因为真正的配置是我们需要测试的东西,而不是一些测试时的设置。

这个问题的根源是@Jon已经指出的差异:通过构造函数注入依赖项是声明性的,而第二个版本使用命令性Service Locator模式。

如果仔细使用IoC容器,则可以静态分析应用程序的运行时配置,而无需实际创建所涉及组件的任何实例。许多流行的容器对此提供了一些变化。Microsoft.Composition是针对.NET 4.5 Web和Metro风格应用程序的MEF版本,CompositionAssert在Wiki文档中提供了一个示例。使用它,您可以编写如下代码:

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

CompositionAssert.CanExportSingle<Foo>(container);

(请参见本示例)。

通过在测试时验证应用程序的成分根,您可能会发现一些错误,这些错误可能会在随后的过程中流失。

希望这是本主题的其他全面答案的有趣补充!


5

注意:我没有完全回答这个问题。但是我觉得这对于依赖注入模式的新学习者会很有用,因为他们对服务定位器(反)模式感到困惑恰巧偶然进入此页面。

我知道服务定位器(现在似乎被视为反模式)和依赖注入模式之间的区别,并且可以理解每种模式的具体示例,但是我对显示构造器内部服务定位器的示例感到困惑(假设我们重新进行构造函数注入)。

“服务定位器”通常既用作模式的名称,又用作引用该模式中使用的对象(也假定)以获取对象而不使用new运算符的名称。现在,也可以在合成根目录使用相同类型的对象用于执行依赖项注入,这就是造成混淆的地方。

关键是要注意的是,您可能正在DI构造函数中使用服务定位器对象,但没有使用“服务定位器模式”。如果有人将其称为IoC容器对象,就不会造成混淆,因为您可能已经猜到它们本质上是在做同样的事情(如果我错了,请纠正我)。

无论是将其称为服务定位器(或仅称为定位器),还是称为IoC容器(或仅称为容器),都没有什么不同,因为您猜测它们可能是指相同的抽象(如果我错了,请更正我) )。只是将其称为服务定位器意味着一个人正在使用服务定位器反模式以及“依赖注入”模式。

恕我直言,将其命名为“定位器”而不是“位置”或“定位”,也会导致人们有​​时认为文章中的服务定位器是指服务定位器容器,而不是服务定位器(反)模式,尤其是当有一个相关的模式称为“依赖注入”而不是“依赖注入”时。


4

在这种过度简化的情况下,没有区别,它们可以互换使用。但是,现实世界中的问题并不是那么简单。只需假设Bar类本身具有另一个名为D的依赖项即可。在这种情况下,您的服务定位器将无法解析该依赖项,因此您必须在D类中实例化它。因为实例化它们的依赖关系是类的责任。如果D类本身具有其他依赖关系,甚至会变得更糟,在现实世界中,它通常甚至比这还要复杂。在这种情况下,DI是比ServiceLocator更好的解决方案。


4
嗯,我不同意:服务定位器。清楚地表明那里仍然存在依赖项...服务定位器。如果bar类本身具有依赖项,那么bar还将具有服务定位器,这就是使用DI / IoC的全部重点。
GFoley83 2014年

2

依赖注入和服务定位器之间有什么区别(如果有)?两种模式都善于实现依赖倒置原则。Service Locator模式在现有代码库中更易于使用,因为它使整体设计更加宽松,而无需强制更改公共接口。出于同样的原因,与基于依赖注入的等效代码相比,基于服务定位器模式的代码可读性较低。

由于签名将具有类(或方法)的签名,因此“依赖注入”模式使之很清楚。由于这个原因,生成的代码更加简洁易读。


1

简单的概念使我对服务定位器和DI容器之间的区别有了更清晰的了解:

  • 服务定位器在使用者中使用,它通过直接使用者的请求从某些存储中按ID 提取服务

  • DI容器位于外部某个地方,它从一些存储中获取服务并将送给消费者(无论是通过构造函数还是通过方法)

但是,我们只能在具体的消费者使用情况下谈论它们之间的区别。在组合根目录中使用Service Locator和DI Container时,它们几乎相似。



-2

作为记录

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

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

  //...
}

除非您真的需要一个接口(该接口被多个类使用),否则一定不要使用它。在这种情况下,IBar允许利用实现它的任何服务类。但是,通常,此接口将由单个类使用。

为什么使用接口是个坏主意?因为确实很难调试。

例如,假设实例“ bar”失败,问:哪个类失败? 我应该修复哪个代码? 一个简单的视图,它导致一个接口,这就是我的路的终点。

相反,如果代码使用硬依赖性,则很容易调试错误。

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

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

  //...
}

如果“ bar”失败,则应检查并触发BarService类。


1
类是构造特定对象的蓝图。另一方面,接口是a contract,仅定义行为而不是动作。而不是传递实际的对象,而是仅共享接口,以便使用者不访问对象的其余部分。同样对于单元测试,它有助于仅测试需要测试的零件。我想随着时间的流逝您会了解它的用处。
Gunhan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.