ServiceLocator 是反模式吗?

Posted

技术标签:

【中文标题】ServiceLocator 是反模式吗?【英文标题】:Is ServiceLocator an anti-pattern? 【发布时间】:2014-05-12 19:00:49 【问题描述】:

最近我阅读了Mark Seemann's article 关于服务定位器反模式的文章。

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

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

    维护问题(这让我很困惑) 考虑以下示例

我们有一个类'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 的情况可能会发生变化。) 请注意,自从发布了这个问题以来,Mark Seemann 的Service Locator is an Anti-Pattern post 有一个更新,描述了服务定位器如何通过破坏封装来违反 OOP,这是他迄今为止最好的论点(以及所有问题的根本原因他在所有先前的论点中使用的症状)。 2015-10-26 更新:Service Locator 的根本问题在于它violates encapsulation。 Here 摘自DIPP&P,总结了Service Locator 及其主要问题。 【参考方案1】:

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

相反,我们必须查看模式是否有效,对于服务定位器,有几个用例。但是,让我们从您给出的示例开始。

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 方法,我们会引入一个 与构造函数中的另一个参数的依赖关系(如果 构造函数注入)。问题仍然存在。

还有两个更严重的问题:

    通过服务定位,您还可以添加另一个依赖项:服务定位器。 您如何判断依赖项应具有的生命周期,以及应如何/何时清理它们?

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

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

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

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

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

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

【讨论】:

Jgauffin,感谢您的回答。您指出了关于启动时自动检查的一件重要事情。我没有想到这一点,现在我看到了 DI 的另一个好处。您还举了一个例子:“var myType = new MyType()”。但我不能把它算作有效的,因为我们从来没有在一个真正的应用程序中实例化依赖项(IoC 容器一直为我们做这件事)。即:在 MVC 应用程序中,我们有一个控制器,它依赖于 IMyService,而 MyServiceImpl 依赖于 IMyRepository。我们永远不会实例化 MyRepository 和 MyService。我们从 Ctor 参数(如从 ServiceLocator)获取实例并使用它们。不是吗? 服务定位器不是反模式的唯一论据是:“如果没有服务定位,控制容器的反转将无法工作”。然而,这个论点是无效的,因为服务定位器是关于意图而不是关于机制的,正如 Mark Seemann here 清楚地解释的那样:“封装在组合根中的 DI 容器不是服务定位器 - 它是一个基础设施组件。”跨度> @jgauffin Web API 不使用服务位置将 DI 用于控制器。它根本不做DI。它的作用是:它使您可以选择创建自己的 ControllerActivator 以传递给配置服务。从那里,您可以创建组合根,无论是纯 DI 还是容器。此外,您正在混合使用作为模式组件的服务位置与“服务定位器模式”的定义。根据该定义,组合根 DI 可以被视为“服务定位器模式”。所以这个定义的全部意义是没有意义的。 我想指出,这是一个总体上很好的答案,我只是评论了你提出的一个误导性观点。 @jgauffin DI 和 SL 都是 IoC 的版本再现。 SL 只是错误的方法。 IoC 容器可能是 SL,或者它可以使用 DI。这取决于它的接线方式。但是SL很糟糕,很糟糕。这是一种隐藏您将所有内容紧密耦合在一起这一事实的方式。【参考方案2】:

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

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

【讨论】:

当您处理遗留代码时,一切都可以让您摆脱混乱,即使这意味着采取中间(和不完美)步骤。服务定位器就是这样的中间步骤。只要你记得good alternative solution exists that is documented, repeatable and proven to be effective,它就可以让你一步一步走出地狱。这种替代解决方案是依赖注入,这正是服务定位器仍然是反模式的原因;正确设计的软件不会使用它。 RE:“当你处理遗留代码时,一切都可以让你摆脱混乱”有时我想知道是否只有一点遗留代码存在,但因为我们可以证明任何事情来解决它,但我们从来没有设法这样做。【参考方案3】:

从测试的角度来看,Service Locator 很糟糕。从 8:45 开始,请参阅 Misko Hevery 的 Google Tech Talk 很好的解释以及代码示例 http://youtu.be/RlfLCWKxHJ0。我喜欢他的比喻:如果你需要 25 美元,就直接要钱,而不是把钱包从取钱的地方拿出来。他还将 Service Locator 比作一个干草堆,里面有你需要的针并且知道如何取回它。使用 Service Locator 的类因此难以重用。

【讨论】:

这是一个回收的意见,作为评论会更好。另外,我认为您(他的?)类比有助于证明某些模式比其他模式更适合某些问题。 我认为该视频非常有用,值得单独回答。评论太容易丢失了。【参考方案4】:

维护问题(这让我很困惑)

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

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

简单明了:其中包含服务定位器的类比通过其构造函数接受其依赖项的类更难重用

假设您需要使用来自LibraryA 的服务,其作者决定使用ServiceLocatorA 和来自LibraryB 的服务,其作者决定使用ServiceLocatorB。除了在我们的项目中使用 2 个不同的服务定位器之外,我们别无选择。如果我们没有好的文档、源代码或快速拨号的作者,需要配置多少个依赖项是一个猜谜游戏。如果这些选项失败,我们可能需要使用反编译器just 来确定依赖项是什么。我们可能需要配置 2 个完全不同的服务定位器 API,并且根据设计,可能无法简单地包装现有的 DI 容器。可能根本不可能在两个库之间共享一个依赖项实例。如果服务定位器没有与我们需要的服务实际驻留在同一个库中,项目的复杂性甚至可能会进一步复杂化——我们隐含地将额外的库引用拖到我们的项目中。

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

Mark Seemann 有一个 *** answer that clearly illustrates this benefit in graphical form,它不仅适用于使用来自另一个库的服务定位器,还适用于在服务中使用外部默认值时。

【讨论】:

【参考方案5】:

是的,服务定位器是violates encapsulation 和solid 的反模式。

【讨论】:

我明白你的意思,但添加更多细节会有所帮助,例如为什么它违反了 SOLID【参考方案6】:

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

这是Adaptive Code Via C#中的一段:

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

-- 霍尔,加里·麦克莱恩。通过 C# 进行自适应代码:采用设计模式和 SOLID 原则的敏捷编码(开发人员参考)(第 309 页)。培生教育。

【讨论】:

【参考方案7】:

作者的理由是“编译器不会帮助你”——这是真的。 当你设计一个类时,你会想要仔细选择它的接口——在其他目标中,让它像......一样独立,因为它是有意义的。

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

隐式进行检查,因此编译器“提供帮助”。 您还无需让客户端了解“定位器”或类似机制的一些信息,因此客户端实际上更加独立。

你说得对,DI 有它的问题/缺点,但提到的优点远远超过它们...... IMO。 你是对的,使用 DI 在接口(构造函数)中引入了一个依赖项 - 但希望这正是你需要的依赖项,并且你想让它变得可见和可检查。

【讨论】:

Zrin,感谢您的想法。据我了解,使用“正确”的 DI 方法,除了单元测试之外,我不应该在任何地方实例化我的依赖项。因此,编译器只会帮助我进行测试。但正如我在最初的问题中所描述的那样,这种破坏测试的“帮助”并没有给我任何东西。是吗? “静态信息”/“编译时检查”参数是个稻草人。正如@davidoff 所指出的,DI 同样容易受到运行时错误的影响。我还要补充一点,现代 IDE 为成员提供了评论/摘要信息的工具提示视图,即使在那些没有的人中,有人仍然会查看文档,直到他们“知道”API。文档就是文档,无论是必需的构造函数参数还是关于如何配置依赖项的备注。 考虑实现/编码和质量保证 - 代码的可读性至关重要 - 特别是对于接口定义。如果您可以在没有自动编译时检查的情况下做到这一点,并且如果您对界面进行了充分的注释/记录,那么我想您至少可以部分弥补隐藏依赖于一种不易看到/可预测的内容的全局变量的缺点。我会说你需要充分的理由来使用这种模式来弥补这些缺点。【参考方案8】:

服务定位器(SL)

Service Locator 解决了[DIP + DI] 问题。它允许通过接口名称满足需求

class A 
  IB ib

  init() 
     ib = ServiceLocator.resolve<IB>();
  

这里的问题是不清楚客户端(A)使用了哪些类(IB的实现)。服务定位器可以作为单例,也可以传递给构造函数。

建议:

class A 
  IB ib

  init(ib: IB) 
     self.ib = ib
  

SL 与 DI IoC 容器(框架)

SL 是关于保存实例,而 DI IoC Container(framework) 更多的是关于创建实例。

SL 在构造函数中检索依赖项时用作 PULL 命令 DI IoC Container(framework) 在将依赖项放入构造函数时用作 PUSH 命令

【讨论】:

以上是关于ServiceLocator 是反模式吗?的主要内容,如果未能解决你的问题,请参考以下文章

深度嵌套字典是反模式吗?

像isInUnitTest()这样的检查是反模式吗?

什么是反模式?

馄饨代码 - 为啥是反模式? [关闭]

为啥 Singleton 被认为是反模式? [复制]

关于“贫血领域模型”被认为是反模式[闭合]的具体例子