ServiceLocator是反模式吗?

时间:2014-04-01 19:45:49

标签: design-patterns dependency-injection anti-patterns service-locator

最近我读过有关Service Locator反模式的Mark Seemann's article

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

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

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

  3. 我们有一个'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方法会遇到相同的运行时问题。

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

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

    更新(摘要):

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

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

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

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

8 个答案:

答案 0 :(得分:114)

如果你将模式定义为反模式只是因为在某些情况下它不适合,那么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();
    }
}

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

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. 如何判断依赖项应该具有哪些生命周期,以及如何/何时清理它们?
  3. 使用容器进行构造函数注入时,可以免费获得。

      

    如果可以的话   忘记设置ServiceLocator,然后我们可能忘记添加一个新的   在我们的Io​​C容器和DI方法中的映射将具有相同的   运行时问题。

    这是真的。但是使用构造函数注入,您不必扫描整个类来确定缺少哪些依赖项。

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

      

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

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

答案 1 :(得分:36)

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

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

答案 2 :(得分:7)

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

答案 3 :(得分:4)

  

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

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

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

      

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

         

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

         

    Mark Seemann有StackOverflow answer that clearly illustrates this benefit in graphical form,它不仅适用于使用其他库中的服务定位器,也适用于在服务中使用外部默认值。

答案 4 :(得分:1)

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

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

  

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

     

- 霍尔,加里麦克莱恩。通过C#的自适应代码:具有设计模式和SOLID原则的敏捷编码(开发人员参考)(p.309)。皮尔逊教育。

答案 5 :(得分:0)

作者的理由&#34;编译器不会帮助你&#34; - 这是真的。 当你设计一个课程时,你会想要仔细选择它的界面 - 除了其他目标之外,还要让它独立......因为它有意义。

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

  • 隐式得到检查,因此编译器&#34;帮助&#34;。
  • 您也不需要客户了解&#34;定位器&#34;或类似的机制,所以客户实际上更独立。

你是对的DI有其问题/缺点,但到目前为止提到的优势超过了他们...... IMO。 你是对的,在DI中,接口(构造函数)中引入了一个依赖关系 - 但希望你需要的依赖关系,以及你想要显示和检查的依赖关系。

答案 6 :(得分:0)

是的,服务定位器是violates encapsulationsolid的反模式。

答案 7 :(得分:0)

服务定位器(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 vs DI IoC 容器(框架)

SL 是关于保存实例,而 DI IoC 容器(框架)更多的是关于创建实例。

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