ServiceLocator是一种反模式吗?

最近我读过Mark Seemann关于Service Locator反模式的文章 。

作者指出了ServiceLocator是反模式的两个主要原因:

  1. API使用问题 (我非常好)
    当类使用Service定位器时,很难看到它的依赖关系,因为在大多数情况下,类只有一个PARAMETERLESS构造函数。 与ServiceLocator相比,DI方法通过构造函数的参数显式地公开依赖关系,因此IntelliSense中很容易看到依赖关系。

  2. 维护问题 (困惑我)
    考虑下面的expample

我们有一个使用服务定位器方法的“MyType”类:

public class MyType { public void MyMethod() { var dep1 = Locator.Resolve<IDep1>(); dep1.DoSomething(); } } 

现在我们想添加另一个依赖类到'MyType'

 public class MyType { public void MyMethod() { var dep1 = Locator.Resolve<IDep1>(); dep1.DoSomething(); // new dependency var dep2 = Locator.Resolve<IDep2>(); dep2.DoSomething(); } } 

这是我误解的起点。 作者说:

要告诉你是否正在引入一个突破性的变化就变得很难了。 您需要了解使用Service Locator的整个应用程序,编译器不会帮助您。

但是等一下,如果我们使用DI方法,我们会在构造函数中引入依赖于另一个参数(在构造函数注入的情况下)。 问题将仍然存在。 如果我们可能忘记设置ServiceLocator,那么我们可能会忘记在IoC容器中添加一个新的映射,而DI方法会有相同的运行时问题。

另外,作者提到了unit testing的困难。 但是,我们不会有DI方法的问题吗? 我们不需要更新实例化该类的所有testing吗? 我们会更新它们来传递一个新的嘲弄的依赖关系,以使我们的testing可以编译。 而且我没有看到更新和时间花费的任何好处。

我不是要捍卫服务定位器的方法。 但这个误解让我觉得我失去了一个非常重要的东西。 有人能消除我的怀疑吗?

更新(摘要):

我的问题“服务定位器是一种反模式”的答案真的取决于具体情况。 而且我绝对不会build议将其从工具列表中划掉。 当您开始处理遗留代码时,它可能变得非常方便。 如果您足够幸运,可以在项目的一开始,那么DI方法可能是一个更好的select,因为它比服务定位器有一些优势。

这里有一些主要的不同之处,使我相信我的新项目不使用服务定位器:

  • 最明显和重要的是:服务定位器隐藏类依赖关系
  • 如果你正在利用一些IoC容器,它可能会在启动时扫描所有的构造函数来validation所有的依赖关系,并为你提供关于缺less映射(或错误configuration)的即时反馈。 如果您将IoC容器用作服务定位器,则这是不可能的

详情请阅读下面给出的优秀答案。

如果你将模式定义为反模式只是因为在某些情况下它不适合,那么是的,这是一个反模式。 但是有了这个推理,所有的模式也将是反模式。

相反,我们必须查看这些模式是否存在有效的用法,而对于服务定位器,有几个用例。 但是让我们先看看你给出的例子。

 public class MyType { public void MyMethod() { var dep1 = Locator.Resolve<IDep1>(); dep1.DoSomething(); // new dependency var dep2 = Locator.Resolve<IDep2>(); dep2.DoSomething(); } } 

这个类的维护噩梦是隐藏的依赖关系。 如果您创build并使用该类:

 var myType = new MyType(); myType.MyMethod(); 

如果使用服务位置隐藏它,则不了解它具有依赖关系。 现在,如果我们使用dependency injection:

 public class MyType { public MyType(IDep1 dep1, IDep2 dep2) { } public void MyMethod() { dep1.DoSomething(); // new dependency dep2.DoSomething(); } } 

你可以直接发现依赖关系,在满足它们之前不能使用这些类。

在典型的业务应用中,您应该避免使用服务位置。 当没有其他select时,应该是使用的模式。

模式是反模式吗?

没有。

例如,如果没有服务地点,控制容器的倒置将不起作用。 这是他们如何解决内部服务。

但更好的例子是ASP.NET MVC和WebApi。 你认为在控制器中dependency injection成为可能吗? 这是正确的 – 服务的位置。

你的问题

但是等一下,如果我们使用DI方法,我们会在构造函数中引入依赖于另一个参数(在构造函数注入的情况下)。 问题将仍然存在。

还有两个更严重的问题:

  1. 随着服务的位置,你还添加另一个依赖项:服务定位器。
  2. 你怎么知道依赖关系应该有哪些生命周期以及应该如何/何时应该清理?

使用容器的构造函数注入,你可以免费获得。

如果我们可能忘记设置ServiceLocator,那么我们可能会忘记在IoC容器中添加一个新的映射,而DI方法会有相同的运行时问题。

确实如此。 但是使用构造函数注入,您不必扫描整个类来找出哪些依赖项丢失。

而且一些更好的容器也会在启动时validation所有依赖(通过扫描所有的构造函数)。 所以这些容器直接得到运行时错误,而不是在稍后的时间点。

另外,作者提到了unit testing的困难。 但是,我们不会有DI方法的问题吗?

不可以。因为您没有对静态服务定位器的依赖关系。 您是否尝试过使用静态依赖关系进行并行testing? 这不好玩。

我还想指出的是,如果你重构的遗留代码,Service Locator模式不仅不是反模式,而且也是一个实际的必要性。 没有人会用数以百万计的代码线来挥舞魔杖,突然之间所有的代码都将被准备好。 所以,如果你想开始将DI引入到现有的代码库中,那么通常情况下,你将会改变事物以慢慢地变成DI服务,并且引用这些服务的代码通常不会是DI服务。 因此,这些服务将需要使用服务定位器来获取那些已被转换为使用DI的服务的实例。

因此,当重构大型遗留应用程序以开始使用DI概念时,我认为Service Locator不仅仅是一种反模式,而且是将DI概念逐渐应用于代码库的唯一方法。

从testing的angular度来看,Service Locator不好。 见代码示例http://youtu.be/RlfLCWKxHJ0 Misey Hevery的Google技术讲座很好的解释从分钟8:45开始。 我喜欢他的比喻:如果你需要25美元,直接问钱而不是把钱从钱包里拿出来。 他还将服务定位器与具有所需针头的草垛进行比较,并知道如何检索它。 使用服务定位器的类很难重用,因为它。

维护问题 (困惑我)

使用服务定位器在这方面有两个不同的原因。

  1. 在你的例子中,你将服务定位器的静态引用硬编码到你的类中。 这将您的类直接耦合到服务定位器,这反过来意味着它不会在没有服务定位器的情况下运行 。 此外,你的unit testing(以及使用这个类的其他人)也隐含地依赖于服务定位器。 有一件事在这里似乎没有被注意到, 当使用构造函数注入时在unit testing时不需要DI容器 ,从而大大简化了unit testing(以及开发人员理解它们的能力)。 这是您使用构造函数注入获得的已实现的unit testing优势。
  2. 至于为什么构造函数Intellisense很重要,这里的人们似乎完全错过了这一点。 一个类只写一次,但可以用于多个应用程序(即多个DIconfiguration) 。 随着时间的推移,如果您可以查看构造函数定义来理解类的依赖关系,而不是查看(希望是最新的)文档,或者如果失败,则返回原始源代码(可能不会方便)来确定类的依赖关系是什么。 带有服务定位器的类通常比较容易编写 ,但是您不仅需要为项目的持续维护付出这种便利的代价。

简单而简单:与其中的服务定位器相比,通过其构造函数接受依赖关系的类更加难以重用

考虑一下你需要使用LibraryA一个服务,它的作者决定使用ServiceLocatorA和一个来自LibraryB的服务,他的作者决定使用ServiceLocatorB 。 除了在我们的项目中使用两个不同的服务定位器,我们别无select。 如果我们没有很好的文档,源代码或快速拨号的作者,需要configuration多less依赖关系是猜谜游戏。 如果没有这些选项,我们可能需要使用反编译器来确定依赖关系是什么。 我们可能需要configuration2个完全不同的服务定位器API,根据devise,可能无法简单地包装现有的DI容器。 在两个库之间共享一个依赖项的实例可能根本不可能。 如果服务定位器没有真正驻留在我们需要的服务所在的同一个库中,那么项目的复杂性甚至会更加复杂 – 我们隐式地将其他库引用拖到我们的项目中。

现在考虑用构造函数注入做的两个相同的服务。 添加对LibraryA的引用。 添加对LibraryB的引用。 在DIconfiguration中提供依赖关系(通过分析Intellisense需要什么)。 完成。

Mark Seemann有一个StackOverflow答案,以graphicsforms清楚地说明了这个好处 ,它不仅适用于使用另一个库的服务定位器,而且在服务中使用外部默认值时也适用。

作者的理由是“编译器不会帮你” – 这是真的。 当你上课的时候,你会想要仔细select自己的界面 – 除了其他目标之外,还要把它作为独立的,因为它是有意义的。

通过让客户端通过显式接口来接受对服务(对依赖)的引用

  • 隐式得到检查,所以编译器“帮助”。
  • 您也不需要客户端了解“定位器”或类似的机制,所以客户端实际上是更独立的。

你说得对,DI有它的问题/缺点,但所提到的优势远远超过他们…… IMO。 你是对的,用DI在接口(构造函数)中引入了一个依赖关系 – 但是这是希望的依赖关系,而且你希望显示和检查依赖关系。

我的知识还不足以判断,但总的来说,我认为如果在特定的情况下某种东西有用处,这并不一定意味着它不能成为反模式。 特别是,当你处理第三方库时,你不能完全控制所有的方面,你最终可能会使用不是最好的解决scheme。

以下是来自适应代码通过C#的一段:

“不幸的是,服务定位器有时是一种不可避免的反模式,在某些应用程序types中,特别是Windows Workflow Foundation,基础结构不适用于构造器注入,在这种情况下,唯一的select是使用服务定位器。对于我所有的(反)模式来说,它比手动构build依赖关系要好得多,毕竟,它仍然可以启用接口提供的所有重要的扩展点,这些扩展点允许装饰器,适配器,和类似的好处。“

– 大厅,加里麦克莱恩。 通过C#编写自适应代码:使用devise模式和SOLID原则进行敏捷编码(开发人员参考)(p。309)。 培生教育。