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

问题描述 投票:257回答:13

这两种模式看起来都像是控制反转原理的实现。也就是说,一个对象不应该知道如何构造它的依赖关系。

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

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

//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容器这一事实的细微差别是什么?

design-patterns dependency-injection service-locator
13个回答
162
投票

差异可能看起来很小,但即使使用ServiceLocator,该类仍然负责创建其依赖项。它只是使用服务定位器来完成它。使用DI,该类被赋予其依赖性。它既不知道,也不关心它们来自何处。这样做的一个重要结果是DI示例更容易进行单元测试 - 因为您可以将其依赖对象的模拟实现传递给它。如果需要,您可以将两者结合起来 - 并注入服务定位器(或工厂)。


3
投票

注意:我并没有完全回答这个问题。但是我觉得这对于依赖注入模式的新学习者来说非常有用,他们对碰巧碰到这个页面的Service Locator (anti-)pattern感到困惑。

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

“服务定位器”通常既用作模式的名称,又用作引用该模式中的对象(假设也是如此)以在不使用新运算符的情况下获取对象的名称。现在,也可以在composition root上使用相同类型的对象来执行依赖注入,这就是混乱的来源。

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

它是被称为服务定位器(或只是定位器),还是作为IoC容器(或只是容器)没有任何区别,因为它们可能指的是相同的抽象(如果我错了,请纠正我) )。只是将其称为服务定位器表明,正在使用Service Locator反模式和依赖注入模式。

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


3
投票

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


1
投票

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

依赖注入模式清楚地表明签名是一个类(或一个方法)将具有哪些依赖关系。因此,生成的代码更清晰,更易读。


-1
投票

作为记录

//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类。


81
投票

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

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

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


44
投票

服务定位器隐藏依赖关系 - 当从对象获取连接时,您无法通过查看对象是否访问数据库(例如)。使用依赖项注入(至少构造函数注入),依赖项是显式的。

此外,服务定位器打破了封装,因为它们提供了对其他对象的依赖关系的全局访问点。有了服务定位器,as with any singleton

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

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


34
投票

Martin Fowler说:

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

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

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

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

你可以查看这篇文章:Dependency Inversion: Service Locator or Dependency Injection

也是经典:Inversion of Control Containers and the Dependency Injection pattern by Martin Fowler

Ralph E. Johnson和Brian Foote的Designing Reusable Classes

然而,睁开眼睛的是:ASP.NET MVC: Resolve or Inject? That’s the Issue… by Dino Esposito


20
投票

使用构造函数DI的类指示消耗代码以满足依赖性。如果类在内部使用SL来检索此类依赖项,则使用代码不会识别依赖项。从表面上看,这似乎更好,但知道任何明确的依赖关系实际上是有帮助的。从架构的角度来看,它更好。在进行测试时,您必须知道某个类是否需要某些依赖项,并配置SL以提供这些依赖项的适当虚假版本。使用DI,只需传入假货。没有太大的区别,但它确实存在。

但是,DI和SL可以协同工作。具有公共依赖关系的中心位置(例如设置,记录器等)是有用的。给定使用此类deps的类,您可以创建一个接收deps的“真实”构造函数,以及一个从SL检索并转发到“real”构造函数的默认(无参数)构造函数。

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


6
投票

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

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

在我的例子中,我使用ServiceLocator这是一个框架组件。并使用Unity作为IoC容器。如果将来我需要将我的IoC容器交换到Ninject我需要做的就是我需要配置我的服务定位器以使用Ninject而不是Unity。轻松迁移。

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


5
投票

我认为这两个一起工作。

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

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


5
投票

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

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

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

主要区别在于依赖关系是如何定位的,在Service Location客户端代码中请求依赖关系,在DI中我们使用容器来创建所有对象,并且它将依赖关系注入构造函数参数(或属性)。


5
投票

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

一旦应用程序由可能数千个组件组成,就很难确定是否可以正确地实例化任何特定组件。通过“正确实例化”,我的意思是在这个基于Foo组件的示例中,IBar的一个实例将可用,并且提供它的组件将:

  • 有必要的依赖,
  • 不参与任何无效的依赖循环,并且
  • 在MEF的情况下,只提供一个实例。

在第二个示例中,构造函数转到IoC容器以检索其依赖项,您可以测试Foo实例能够使用应用程序的实际运行时配置正确实例化的唯一方法是实际构建它。

这在测试时具有各种尴尬的副作用,因为在运行时工作的代码不一定在测试工具下工作。 Mocks不会这样做,因为真正的配置是我们需要测试的东西,而不是一些测试时间设置。

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

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

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

CompositionAssert.CanExportSingle<Foo>(container);

(见this example)。

通过在测试时验证应用程序的组合根,您可能会捕获一些错误,否则这些错误可能会在以后的过程中通过测试。

希望这是关于这个主题的这个全面的答案的一个有趣的补充!

© www.soinside.com 2019 - 2024. All rights reserved.