ServiceLocator是反模式吗?


137

最近,我阅读了Mark Seemann的有关Service Locator反模式的文章

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

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

  2. 维护问题(使我感到困惑)
    请考虑以下示例

我们有一个使用服务定位器方法的“ 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();
    }
}

这就是我的误会开始的地方。作者说:

判断您是否要进行重大更改变得更加困难。您需要了解正在使用服务定位器的整个应用程序,并且编译器将无法为您提供帮助。

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

另外,作者提到了单元测试的困难。但是,我们不会对DI方法有疑问吗?我们是否不需要更新所有实例化该类的测试?我们将更新它们以通过新的模拟依赖项,以使我们的测试可编译。我看不到该更新和时间花费有任何好处。

我不是要捍卫服务定位器方法。但是这种误解使我认为我正在失去一些非常重要的东西。有人可以消除我的怀疑吗?

更新(摘要):

我的问题“服务定位器是否是反模式”的答案实际上取决于情况。而且我绝对不建议从您的工具列表中删除它。当您开始处理遗留代码时,它可能会变得非常方便。如果您有幸进入项目的开始,那么DI方法可能会是一个更好的选择,因为它比Service Locator更具优势。

以下是使我确信不要对新项目使用Service Locator的主要区别:

  • 最明显和最重要的:服务定位器隐藏类依赖关系
  • 如果您正在使用某些IoC容器,它可能会在启动时扫描所有构造函数以验证所有依赖关系,并就缺少的映射(或错误的配置)立即提供反馈;如果您将IoC容器用作服务定位器,则这是不可能的

有关详细信息,请阅读下面给出的出色答案。


“我们是否不需要更新所有实例化该类的测试?” 如果您在测试中使用构建器,则不一定正确。在这种情况下,您只需更新构建器。
彼得·卡尔森

您说对了,要看情况。例如,在大型Android应用中,由于低规格移动设备的性能问题,人们一直不愿使用DI。在这种情况下,您必须找到仍然编写可测试代码的替代方法,在这种情况下,Service Locator是一个很好的替代方法。(注意:当新的Dagger 2.0 DI框架足够成熟时,Android可能会发生变化。)
G. Lombard 2015年

1
请注意,由于已经发布了此问题,因此Mark Seemann的Service Locator上有一个更新,它是一个反模式的帖子,描述了Service locator如何通过破坏封装来违反OOP,这是他迄今为止最好的论点(以及所有症状的根本原因)他在所有先前的论点中都使用过)。2015年10月26日更新:服务定位器的基本问题是它违反了封装
NightOwl888 '16

Answers:


125

如果仅在某些情况下将模式定义为反模式,则说是反模式,是的。但是有了这种推理,所有模式也将成为反模式。

相反,我们必须查看模式的有效用法,并且对于Service Locator,有几种用例。但是,让我们先看一下您给出的示例。

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

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

该类的维护噩梦是依赖项被隐藏。如果创建并使用该类:

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

如果使用服务位置将它们隐藏,则您将无法理解它具有依赖关系。现在,如果我们改为使用依赖项注入:

public class MyType
{
    public MyType(IDep1 dep1, IDep2 dep2)
    {
    }

    public void MyMethod()
    {
        dep1.DoSomething();

        // new dependency
        dep2.DoSomething();
    }
}

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

出于这种原因,在典型的业务应用程序中,应避免使用服务位置。没有其他选项时,应使用该模式。

模式是反模式吗?

没有。

例如,没有服务位置,控制容器的反转将无法工作。这就是他们内部解决服务的方式。

但是更好的例子是ASP.NET MVC和WebApi。您如何看待控制器中的依赖项注入?是的-服务位置。

你的问题

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

还有两个更严重的问题:

  1. 使用服务位置,您还添加了另一个依赖项:服务定位器。
  2. 您如何确定依赖项的生命周期,以及如何/何时清理依赖项?

通过使用容器的构造函数注入,您可以免费获得它。

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

确实如此。但是通过构造函数注入,您不必扫描整个类即可确定缺少哪些依赖项。

一些更好的容器还可以在启动时验证所有依赖项(通过扫描所有构造函数)。因此,使用这些容器,您将直接获得运行时错误,而不是在稍后的某个时间点。

另外,作者提到了单元测试的困难。但是,我们不会对DI方法有疑问吗?

不。因为您不依赖于静态服务定位器。您是否尝试过使用静态依赖项进行并行测试?不好玩


2
Jgauffin,谢谢您的回答。您指出了启动时自动检查的一件事。我没有考虑这一点,现在我看到了DI的另一个好处。您还给出了一个示例:“ var myType = new MyType()”。但是我不能认为它是有效的,因为我们从来没有实例化真实应用程序中的依赖关系(IoC容器一直在为我们做它)。即:在MVC应用中,我们有一个控制器,该控制器依赖于IMyService,而MyServiceImpl依赖于IMyRepository。我们永远不会实例化MyRepository和MyService。我们从Ctor参数中获取实例(例如从ServiceLocator中获取)并使用它们。不是吗
davidoff 2014年

33
对于Service Locator不是反模式的唯一论点是:“没有服务位置,控制容器的反转将无法工作”。但是,该论点是无效的,因为Service Locator与意图有关,而不与机制有关,正如Mark Seemann 在此处清楚地解释的那样:“封装在Composition Root中的DI容器不是Service Locator-它是基础结构组件。”
史蒂文

4
@jgauffin Web API不会将服务位置用于DI到Controller。它根本不做DI。它的作用是:它使您可以选择创建自己的ControllerActivator以传递到Configuration Services。从那里,您可以创建合成根,无论它是Pure DI还是Containers。另外,您还将服务定位的使用(作为模式的组成部分)与“服务定位器模式”的定义混合在一起。根据该定义,合成根目录DI可以被视为“服务定位器模式”。因此,该定义的全部内容尚无定论。
Suamere 2014年

1
我确实想指出,这是一个普遍的好答案,我只是评论了您提出的一个误导性观点。
Suamere 2014年

2
@jgauffin DI和SL都是IoC的版本。SL是错误的方法。IoC容器可以是SL,也可以使用DI。这取决于其接线方式。但是SL不好,坏不好。这是一种掩盖您将所有事物紧密结合在一起的事实的方法。
MirroredFate

37

我还想指出,如果您要重构遗留代码,那么Service Locator模式不仅是一种反模式,而且是实际需要。没有人会在数百万行代码中挥舞着魔杖,突然所有的代码都准备好进行DI。因此,如果您想开始将DI引入现有代码库中,通常情况是您会慢慢将事物更改为DI服务,并且引用这些服务的代码通常不是DI服务。因此,THOSE服务将需要使用服务定位器,以获取已转换为使用DI的那些服务的实例。

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


12
当您处理遗留代码时,一切都可以使您摆脱混乱,即使这意味着采取中间(且不完善)的步骤。服务定位器就是这样的中间步骤。只要您记得存在一个有据可查,可重复且证明有效好的替代解决方案,它就可以使您一次摆脱困境。这种替代解决方案是“依赖注入”,这正是服务定位器仍是反模式的原因。设计正确的软件不会使用它。
史蒂文

RE:“当您处理遗留代码时,一切都是合理的,以使您摆脱困境”有时我想知道是否只存在过一点遗留代码,但是因为我们可以证明有任何理由对其进行修复,我们不知何故从没有做到过。
Drew Delano

8

从测试的角度来看,服务定位器很糟糕。请参见Misko Hevery的Google Tech Talk很好的解释,其中包含代码示例,网址为http://youtu.be/RlfLCWKxHJ0,始于8:45分钟。我喜欢他的比喻:如果您需要25美元,请直接索要钱,而不是从要取钱的地方给您的钱包。他还将“服务定位器”与具有所需针头的干草堆进行了比较,并且知道如何将其取回。因此,使用Service Locator的类很难重用。


10
这是一个可循环使用的意见,作为注释本来会更好。另外,我认为您的(他的?)类推可以证明某些模式比其他模式更适合某些问题。
8bitjunkie 2014年

6

维护问题(使我感到困惑)

在这方面,使用服务定位器很糟糕的原因有两个。

  1. 在您的示例中,您正在将对服务定位器的静态引用硬编码到您的类中。这将您的班级直接与服务定位器紧密耦合,这反过来意味着没有服务定位器,它将无法运行。此外,您的单元测试(以及使用该类的其他任何人)也隐式依赖于服务定位器。在这里似乎没有引起注意的一件事是,使用构造函数注入时在单元测试时不需要DI容器,这大大简化了单元测试(以及开发人员理解它们的能力)。那就是您从使用构造函数注入中获得的已实现的单元测试收益。
  2. 至于为什么构造函数Intellisense如此重要,这里的人们似乎完全忽略了这一点。一个类只编写一次,但是可以在多个应用程序中使用(即,几个DI配置)。随着时间的流逝,如果您可以查看构造函数定义以了解类的依赖关系,而不是查看(希望是最新的)文档,否则,如果返回到原始源代码(可能不会方便)以确定类的依赖项。使用服务定位器的类通常更容易编写,但是您在进行项目的持续维护中为此付出的代价远远超过了付出的代价。

简单明了:带有服务定位符的类是 通过其构造函数接受其依赖关系更难重用

考虑一下您需要使用LibraryA其作者决定使用ServiceLocatorA的服务以及来自的服务的情况LibraryB的情况ServiceLocatorB。除了在我们的项目中使用2个不同的服务定位器外,我们别无选择。如果我们没有好的文档,源代码或快速拨号的作者,那么需要配置多少依赖项是一个猜谜游戏。失败的这些选项,我们可能需要使用反编译器只是找出依赖关系是什么。我们可能需要配置2个完全不同的服务定位器API,根据设计的不同,可能无法简单地包装现有的DI容器。可能根本不可能在两个库之间共享一个依赖项实例。如果服务定位符实际上并没有与我们所需的服务位于同一库中,则项目的复杂性甚至可能进一步加重-我们正在隐式将其他库引用拖到我们的项目中。

现在考虑使用构造函数注入进行的两个相同的服务。添加对的引用LibraryA。添加参考LibraryB。在DI配置中提供依赖项(通过Intellisense分析需要的内容)。做完了

马克·西曼(Mark Seemann)有一个StackOverflow答案,以图形形式清楚地说明了这种好处,不仅适用于使用另一个库中的服务定位符,而且适用于服务中的外部默认值。


1

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

这是通过C#自适应代码的一段:

“不幸的是,服务定位器有时是不可避免的反模式。在某些应用程序类型(尤其是Windows Workflow Foundation)中,基础结构不适合进行构造函数注入。在这些情况下,唯一的选择是使用服务定位器。这是总比不注入依赖项好。对于我所有针对(anti-)模式的硫酸,它比手动构建依赖项好得多。毕竟,它仍然启用由接口提供的那些最重要的扩展点,这些扩展点允许装饰器,适配器,和类似的好处。”

-Hall,Gary McLean。通过C#进行的自适应代码:具有设计模式和SOLID原理的敏捷编码(开发人员参考)(第309页)。培生教育。


0

作者的理由是“编译器无法帮助您”-的确如此。当设计一个班级时,您将需要仔细选择它的界面-在其他目标中,使其尽可能地独立于……。

通过让客户端通过显式接口接受对服务(对依赖项)的引用,您可以

  • 隐式地得到检查,因此编译器“有所帮助”。
  • 您还不需要客户端了解有关“定位器”或类似机制的知识,因此客户端实际上更加独立。

没错,DI有它的问题/缺点,但是提到的优点远远超过了它们……IMO。没错,使用DI在接口(构造函数)中引入了一个依赖关系-希望这是您需要的,并且希望使其可见和可检查的依赖关系。


兹林,谢谢您的想法。据我了解,使用“适当的” DI方法,除了单元测试之外,我不应该在任何地方实例化我的依赖项。因此,编译器仅对我的测试有帮助。但是,正如我在原始问题中所描述的那样,测试失败的“帮助”没有任何帮助。可以?
davidoff 2014年

“静态信息” /“编译时检查”参数是一个稻草人。正如@davidoff指出的那样,DI同样容易受到运行时错误的影响。我还要补充一点,现代的IDE为成员提供评论/摘要信息的工具提示视图,即使在那些不这样的视图中,直到他们“了解” API之前,人们仍然会在查看文档。文档是文档,无论是必需的构造函数参数还是有关如何配置依赖项的说明。
tuespetre 2014年

考虑实现/编码和质量保证-代码的可读性至关重要-特别是对于接口定义。如果您可以在没有自动编译时间检查的情况下进行操作,并且可以对界面做充分的注释/记录,那么我想您可以至少部分地弥补对一种不容易看到/可预测的内容的全局变量的隐藏依赖的缺点。我想说您有充分的理由使用这种模式,要胜过这种缺点。
Zrin 2014年

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.