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

Posted

技术标签:

【中文标题】依赖注入和服务定位器模式有啥区别?【英文标题】:What's the difference between the Dependency Injection and Service Locator patterns?依赖注入和服务定位器模式有什么区别? 【发布时间】:2009-10-13 01:15:19 【问题描述】:

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

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

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

//Foo Needs an IBar
public class Foo

  private IBar bar;

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

  //...

Service Locator 似乎使用了一个“容器”,它连接了它的依赖关系并为 foo 提供了它的 bar。

使用服务定位器的示例:

//Foo Needs an IBar
public class Foo

  private IBar bar;

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

  //...

因为我们的依赖关系只是对象本身,所以这些依赖关系有依赖关系,它们有更多的依赖关系,依此类推。于是,Inversion of Control Container(或DI Container)诞生了。示例:Castle Windsor、Ninject、Structure Map、Spring 等)

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

【问题讨论】:

控制反转意味着“一个对象不应该知道如何构造它的依赖关系”?!?那个对我来说是新的。不,真的,这不是“控制反转”的意思。请参阅 martinfowler.com/bliki/InversionOfControl.html 那篇文章甚至提供了该术语的词源参考,可以追溯到 1980 年代。 在这里回答:infoq.com/articles/Succeeding-Dependency-Injection Mark Seemann 认为服务定位器是反模式 (blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern)。此外,我发现图表(在此处找到,***.com/a/9503612/1977871)有助于理解 DI 和 SL 困境。希望这会有所帮助。 【参考方案1】:

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

【讨论】:

另外,您可以在构造类时同时使用两者。默认构造函数可以使用 SL 检索依赖项,并将它们传递给接收这些依赖项的“真实”构造函数。您可以两全其美。 不,ServiceLocator 负责实例化给定依赖项(插件)的正确实现。在 DI 的情况下,DI“容器”是负责的。 @Rogerio 是的,但是班级仍然必须了解服务定位器……这是两个依赖项。此外,我经常看到 Service Locator 委托给 DI 容器进行查找,特别是对于需要服务支持的临时对象。 @Adam 我没有说服务定位器会委托给 DI 容器。这是两个相互排斥的模式,如"official" article 中所述。对我来说,Service Locator 在实践中比 DI 有一个巨大的优势:使用 DI 容器会导致滥用(我已经多次看到),而使用 Service Locator 则不会。 “这样做的一个重要结果是 DI 示例更容易进行单元测试——因为您可以将其依赖对象的模拟实现传递给它。”不对。在您的单元测试中,可以使用对服务定位器容器中的注册函数的调用来轻松地将模拟添加到注册表中。【参考方案2】:

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

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

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

【讨论】:

但是您如何处理必须在运行时创建对象的情况?如果您使用“new”手动创建它们,则无法使用 DI。如果你调用 DI 框架寻求帮助,你就打破了这种模式。那么还有哪些选择呢? @Boris 我遇到了同样的问题并决定注入特定于类的工厂。不漂亮,但完成了工作。希望看到更漂亮的解决方案。 比较直接链接:martinfowler.com/articles/… @Boris 如果我需要即时构建新对象,我会为所述对象注入一个抽象工厂。这类似于在此实例中注入服务定位器,但提供了一个具体的、统一的、编译时的接口来构建相关对象并使依赖关系显式化。【参考方案3】:

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

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

很难指定 pre 和 post 客户对象的条件 接口,因为它的工作原理 实施可以干预 从外面。

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

【讨论】:

我更喜欢“单身人士被认为是愚蠢的”,steve.yegge.googlepages.com/singleton-considered-stupid 我喜欢史蒂夫·耶格,那篇文章的标题很棒,但我认为我引用的那篇文章和 Miško Hevery 的“单身人士是病态的骗子”(misko.hevery.com/2008/08/17/singletons-are-pathological-liars) 可以更好地反对服务定位器的特殊功能。 这个答案是最正确的,因为它最好地定义了一个服务定位器:“一个隐藏其依赖关系的类。”请注意,在内部创建依赖项虽然通常不是一件好事,但不会使类成为服务定位器。此外,对容器的依赖是一个问题,但不是最清楚地定义服务定位器的“问题”。 With dependency injection (at least constructor injection) the dependencies are explicit.。请解释一下。 如上所述,我看不出 SL 如何使依赖关系不如 DI 明确...【参考方案4】:

Martin Fowler 说

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

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

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

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

你可以查看这个帖子:Dependency Inversion: Service Locator or Dependency Injection

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

Designing Reusable Classes Ralph E. Johnson 和 Brian Foote

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

【讨论】:

精彩总结:“服务定位器和依赖注入只是依赖倒置原则的实现。” 他还说:关键的区别在于服务定位器的每个用户都依赖于定位器。定位器可以隐藏对其他实现的依赖关系,但您确实需要查看定位器。所以定位器和注入器之间的决定取决于该依赖是否是一个问题。 ServiceLocator 和 DI 与“依赖倒置原则”(DIP)无关。 DIP 是一种使高级组件更具可重用性的方法,通过将编译时对低级组件的依赖替换为对与高级组件一起定义的抽象类型的依赖,该抽象类型由低级组件实现水平组件;通过这种方式,编译时依赖被反转,因为现在它是依赖于高级依赖的低级依赖。另外,请注意 Martin Fowler 的文章解释说 DI 和 IoC不是相同的东西。【参考方案5】:

使用构造函数 DI 的类向消费代码表明存在需要满足的依赖关系。如果该类在内部使用 SL 来检索此类依赖项,则使用代码不会意识到这些依赖项。这可能表面上看起来更好,但实际上了解任何显式依赖关系是有帮助的。从建筑的角度来看,它更好。并且在进行测试时,您必须知道一个类是否需要某些依赖项,并配置 SL 以提供这些依赖项的适当假版本。使用 DI,只需传递假货。差别不大,但确实存在。

不过,DI 和 SL 可以一起工作。为常见依赖项(例如设置、记录器等)提供一个中心位置很有用。给定一个使用此类 deps 的类,您可以创建一个接收 deps 的“真实”构造函数,以及一个从 SL 检索并转发给“真实”构造函数的默认(无参数)构造函数。

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

【讨论】:

有趣!我同时使用 DI 和 SL,但没有使用两个构造函数。三到四个最无聊的经常需要的依赖项(设置等)从 SL 中获得。其他一切都被构造函数注入。这有点难看,但很实用。【参考方案6】:

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

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

Service locator 和 DI Container 看起来更相似,都是使用容器来定义依赖关系,将抽象映射到具体实现。

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

【讨论】:

依赖注入不需要使用 DI 容器。使用 DI,一种不常见的方法是使用 Pure DI,这基本上意味着您在 Composition Root 中手动连接您的对象图。【参考方案7】:

在我的上一个项目中,我同时使用了两者。 我使用依赖注入来实现单元可测试性。我使用服务定位器来隐藏实现并依赖于我的 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/

【讨论】:

johandekoning 文章链接已损坏。【参考方案8】:

添加一个理由,灵感来自我们上周为 MEF 项目编写的文档更新(我帮助构建 MEF)。

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

有其所需的依赖关系, 不参与任何无效的依赖循环,并且 对于 MEF,仅提供一个实例。

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

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

这个问题的根源是@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)。

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

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

【讨论】:

【参考方案9】:

我认为两者可以协同工作。

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

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

【讨论】:

【参考方案10】:

以下简单的概念让我对 Service Locator 和 DI Container 之间的区别有了更清晰的理解:

服务定位器在消费者中使用,它根据消费者的直接请求从一些存储中按 ID 服务

DI Container 位于外部某处,它从一些存储中获取服务并推送它们给消费者(无论是通过构造函数还是通过方法)

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

【讨论】:

【参考方案11】:

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

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

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

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

无论它被称为服务定位器(或只是定位器),还是作为 IoC 容器(或只是容器)都没有你猜到的区别,它们可能指的是相同的抽象(如果我'我错了)。只是将其称为服务定位器意味着将服务定位器反模式与依赖注入模式一起使用。

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

【讨论】:

【参考方案12】:

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

【讨论】:

嗯,我不同意:服务定位器 ex。清楚地表明那里仍然存在依赖关系......服务定位器。如果bar 类本身有依赖关系,那么bar 也会有服务定位器,这就是使用DI/IoC 的重点。【参考方案13】:

服务定位器和依赖注入都是对象访问模式实现,遵循依赖倒置原则


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

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


如果您需要处理 [动态结构],例如 [ui 树] 或任何 [分形设计的应用程序],您可能需要 Service Locator。

例子:

React 的 createContext/useContext 提供/注入 Vue 角度提供者

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

例子:

C#/Java 中的注解

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

当您知道它是 提供该服务的静态容器时使用 DI。


Service Locator 模式更像是模块级别 Dependency Providers,而DI 是全局级别

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

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


个人建议:

    尽可能使用 DI。 如果您必须处理分形结构中的动态/运行时服务解析,请使用服务定位器。 将服务定位器封装为作用域 DI,例如:@inject( scope: 'contextual' ) 通过接口而不是类/构造函数定位服务。

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

【讨论】:

【参考方案14】:

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

依赖注入模式清楚地说明了类(或方法)将具有哪些依赖项的签名。出于这个原因,生成的代码更干净,更易读。

【讨论】:

【参考方案15】:

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

【讨论】:

【参考方案16】:

记录在案

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

【讨论】:

类是构造特定对象的蓝图。另一方面,接口是contract,仅定义行为而不是操作。与其传递实际对象,不如只共享接口,这样消费者就不会访问对象的其余部分。同样对于单元测试,它有助于仅测试需要测试的部分。我想你迟早会明白它的用处。

以上是关于依赖注入和服务定位器模式有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Ninject、MVC 3 和使用服务定位器模式的依赖注入

AngularJS(15)-依赖注入

AngularJS 依赖注入

接口和抽象之间有啥区别以及依赖注入如何[重复]

使用哪种模式进行日志记录?依赖注入还是服务定位器?

AngularJS依赖注入