ServiceLocator是反模式吗?

dav*_*off 127 design-patterns dependency-injection anti-patterns service-locator

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

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

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

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

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

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

现在我们要为类'MyType'添加另一个依赖项

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

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}
Run Code Online (Sandbox Code Playgroud)

这就是我的误解开始的地方.作者说:

要判断你是否引入了一个重大改变,要变得更加困难.您需要了解使用Service Locator的整个应用程序,并且编译器不会帮助您.

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

此外,作者还提到了单元测试的难点.但是,我们不会有DI方法的问题吗?我们不需要更新所有实例化该类的测试吗?我们将更新它们以传递一个新的模拟依赖项,以使我们的测试可编译.我没有看到更新和时间花费带来任何好处.

我不是想捍卫Service Locator方法.但这种误解让我觉得我失去了一些非常重要的东西.有人可以消除我的怀疑吗?

更新(摘要):

我的问题"服务定位器是反模式"的答案实际上取决于具体情况.我绝对不会建议你从工具列表中删除它.当您开始处理遗留代码时,它可能会变得非常方便.如果你很幸运能够处于项目的最初阶段,那么DI方法可能是更好的选择,因为它比Service Locator有一些优势.

以下是主要的不同之处,这些差异使我不相信我的新项目使用Service Locator:

  • 最明显和最重要的是:Service Locator隐藏了类依赖项
  • 如果你正在使用一些IoC容器,它可能会在启动时扫描所有构造函数以验证所有依赖项,并立即给你关于缺失映射(或错误配置)的反馈; 如果您将IoC容器用作服务定位器,则无法进行此操作

有关详细信息,请阅读下面给出的优秀答案.

jga*_*fin 120

如果你将模式定义为反模式只是因为在某些情况下它不适合,那么YES就是反模式.但由于这种推理,所有模式也都是反模式.

相反,我们必须查看模式是否有有效用法,而对于Service Locator,有几个用例.但是,让我们先看看你给出的例子.

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

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

var myType = new MyType();
myType.MyMethod();
Run Code Online (Sandbox Code Playgroud)

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

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

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

        // new dependency
        dep2.DoSomething();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在典型的业务应用程序中,您应该避免使用服务位置.它应该是没有其他选项时使用的模式.

这种模式是一种反模式吗?

没有.

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

但更好的例子是ASP.NET MVC和WebApi.您认为控制器中的依赖注入可能是什么?那是对的 - 服务地点.

你的问题

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

还有两个更严重的问题:

  1. 使用服务位置,您还要添加另一个依赖项:服务定位器.
  2. 您如何判断依赖项应该具有哪些生命周期,以及它们应该如何/何时清理?

使用容器构造函数注入,您可以免费获得.

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

确实如此.但是使用构造函数注入,您不必扫描整个类来确定缺少哪些依赖项.

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

此外,作者还提到了单元测试的难点.但是,我们不会有DI方法的问题吗?

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

  • 您唯一的服务定位器不是反模式的论点是:"没有服务位置,控制容器的反转将无法工作".然而,这个论点是无效的,因为服务定位器是关于意图而不是关于机制的,正如Mark Seemann在[here](http://blog.ploeh.dk/2011/08/25/ServiceLocatorrolesvs.mechanics/)中所解释的那样:"封装在Composition Root中的DI容器不是服务定位器 - 它是一个基础架构组件." (28认同)
  • @jgauffin Web API不将DI的服务位置用于控制器.它根本不做DI.它的作用是:它为您提供了创建自己的ControllerActivator以传递到配置服务的选项.从那里,您可以创建组合根,无论是Pure DI还是Containers.此外,您将服务位置(作为模式的一个组件)与"服务定位器模式"的定义混合使用.根据该定义,组合根DI可以被视为"服务定位器模式".所以这个定义的重点是没有实际意义的. (4认同)
  • Jgauffin,谢谢你的回答.你在启动时指出了一个关于自动检查的重要事项.我没有想到这一点,现在我看到了DI的另一个好处.您还举了一个例子:"var myType = new MyType()".但我无法将它视为有效的,因为我们从未在真实的应用程序中实例化依赖项(IoC Container一直为我们做这件事).即:在MVC应用程序中,我们有一个控制器,它依赖于IMyService,而MyServiceImpl依赖于IMyRepository.我们永远不会实例化MyRepository和MyService.我们从Ctor params(例如来自ServiceLocator)获取实例并使用它们.不是吗? (2认同)
  • @jgauffin DI和SL都是IoC的版本.SL只是*错误*的方式来做到这一点.IoC容器**可能是SL,或者它可以使用DI.这取决于它的连线方式.但SL很糟糕,糟糕.这是一种隐藏你将所有东西紧密结合在一起的事实的方式. (2认同)

jwe*_*313 37

我还想指出,如果你重构遗留代码,服务定位器模式不仅不是反模式,而且它也是一种实际需要.没有人会在数百万行代码上挥动魔杖,突然所有的代码都准备就绪了.因此,如果您想开始将DI引入现有代码库,通常情况下您会将内容更改为DI服务,并且引用这些服务的代码通常不会是DI服务.因此,这些服务将需要使用服务定位器,以获取已转换为使用DI的那些服务的实例.

因此,当重构大型遗留应用程序以开始使用DI概念时,我会说服务定位器不仅不是反模式,而且它是将DI概念逐渐应用于代码库的唯一方法.

  • 当您处理遗留代码时,即使这意味着采取中间(和不完美)步骤,一切都有理由让您摆脱困境.服务定位器就是这样的中间步骤.只要你记得[存在一个记录良好,可重复且被证明有效的良好替代解决方案],它就可以让你在当时一步到位:(https://en.wikipedia.org/wiki/反模式).这种替代解决方案是依赖注入,这正是服务定位器仍然是反模式的原因; 设计合理的软件不会使用它. (10认同)
  • RE:“当你处理遗留代码时,一切都是合理的,可以让你摆脱困境”有时我想知道是否只有一点点遗留代码曾经存在过,但是因为我们可以证明任何事情都可以修复它,所以我们不知何故从未设法做到这一点。 (2认同)

小智 7

从测试的角度来看,Service Locator很糟糕.请参阅Misko Hevery的Google Tech Talk,并在8:45分钟开始使用代码示例http://youtu.be/RlfLCWKxHJ0.我喜欢他的比喻:如果你需要25美元,直接要钱,而不是从钱包里拿钱包.他还将服务定位器与具有您需要的针头的干草堆进行比较,并知道如何检索它.使用Service Locator的类很难重用,因为它.

  • 这是一个回收的意见,作为评论本来会更好.另外,我认为你的(他的?)类比可以证明某些模式比其他模式更适合某些问题. (10认同)
  • 我认为该视频足够有用,值得单独回答。评论太容易丢失了。 (5认同)

Nig*_*888 5

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

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

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

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

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

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

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