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

时间:2009-10-13 01:15:19

标签: design-patterns dependency-injection service-locator

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

依赖注入(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容器的细微差别是什么?

16 个答案:

答案 0 :(得分:157)

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

答案 1 :(得分:80)

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

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

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

答案 2 :(得分:42)

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

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

  

指定前置和后置变得困难   客户对象的条件   界面,因为它的运作   实施可以插手   来自外面。

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

答案 3 :(得分:32)

Martin Fowler陈述

  

使用服务定位器,应用程序类通过a显式请求它   消息到定位器。注射没有明确的要求,   服务出现在应用程序类中 - 因此反转   控制。

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

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

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

您可以查看此信息:Dependency Inversion: Service Locator or Dependency Injection

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

拉尔夫·约翰逊(Ralph E. Johnson)撰写的Designing Reusable Classes&amp; Brian Foote

然而,让我眼前一亮的是:ASP.NET MVC: Resolve or Inject? That’s the Issue… by Dino Esposito

答案 4 :(得分:19)

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

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

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

答案 5 :(得分: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/

答案 6 :(得分:5)

我认为两者合作。

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

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

答案 7 :(得分:5)

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

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

  • 具有所需的依赖关系,
  • 不涉及任何无效的依赖循环,
  • 在MEF的情况下,只提供一个实例。

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

这在测试时有各种各样的尴尬副作用,因为在运行时工作的代码不一定在测试工具下工作。 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)。

通过在测试时验证应用程序的 Composition Roots ,您可能会捕获一些错误,否则这些错误可能会在此过程的后期进行测试。

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

答案 8 :(得分:5)

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

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

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

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

答案 9 :(得分:3)

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

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

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

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

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

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

答案 10 :(得分:3)

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

答案 11 :(得分:1)

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

依赖注入模式清楚地表明了签名与类(或方法)的依赖关系。因此,生成的代码更清晰,更易读。

答案 12 :(得分:1)

服务定位器和依赖注入都是遵循依赖倒置原则

对象访问模式实现

依赖注入是[静态/全局]对象访问模式

服务定位器是[动态]对象访问模式


如果您需要处理 [动态结构],如 [ui 树] 或任何 [分形设计的应用程序],您可能需要服务定位器。

示例:

  • react 的 createContext/useContext
  • 提供/注入 Vue
  • angular 的提供者

如果您只想从您的类中获取一个不关心应用程序的层次结构实例在该层次结构中的位置的实例,您应该使用 DI。

示例:

  • C#/Java 中的注解

当您在运行前不知道服务的实际提供者时使用服务定位器。

DI 在您知道提供服务的静态容器时使用。


服务定位器模式更像是模块级别依赖提供者,而 DI 是全局级别

当有一个子模块声明应该由它的父模块提供的服务的依赖关系而不是静态解析类型(单例/transient/static-scoped).

它可以通过DI的范围注入模式实现,而范围由应用程序的模块结构/关系定义。


个人建议:

  1. 尽可能使用 DI。
  2. 如果您必须处理分形结构中的动态/运行时服务解析,请使用服务定位器。
  3. 将 Service Locator 封装为一个 scoped DI,例如:@inject({ scope: 'contextual' })
  4. 通过接口而不是类/构造函数来定位服务。

详细信息:https://docs.microsoft.com/zh-cn/dotnet/core/extensions/dependency-injection-guidelines#recommendations

答案 13 :(得分:0)

以下简单的概念使我对SL和DI之间的区别有了更清晰的理解(我只记得一个粗体的词):

  消费者使用的

SL,是直接消费者的请求通过ID从某些存储中服务; DI位于外部某个地方,它从一些存储中获取服务,并推动将其提供给消费者(无论是通过构造函数还是通过方法)

但是,我们只能在具体的消费者使用情况下谈论它们之间的区别;当SL和DI用于成分根时,它们几乎相似。

答案 14 :(得分:0)

DI容器是服务定位器的超集。它可以用于查找服务,并具有组装(连接)依赖项注入的附加功能。

答案 15 :(得分:-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类。