首页 文章

ServiceLocator是反模式吗?

提问于
浏览
121

最近我读了关于Service Locator反模式的Mark Seemann's article .

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

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

  • Maintenance issue (这让我很困惑)
    请考虑以下示例

我们有一个使用服务定位器方法的类'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方法 . 但这种误解让我觉得我失去了一些非常重要的东西 . 有人可以消除我的怀疑吗?

UPDATE (SUMMARY):

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

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

  • 最明显和最重要的是:Service Locator隐藏了类依赖项

  • 如果您正在使用某个IoC容器,它可能会在启动时扫描所有构造函数以验证所有依赖项,并为您提供有关丢失映射(或错误配置)的即时反馈;如果您将IoC容器用作服务定位器,则无法实现此目的

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

7 回答

  • 3

    如果你将模式定义为反模式只是因为在某些情况下它不适合,那么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方法,我们将在构造函数中引入与另一个参数的依赖关系(在构造函数注入的情况下) . 问题仍然存在 .

    还有两个更严重的问题:

    • 对于服务位置,您还要添加另一个依赖项:服务定位器 .

    • 如何判断依赖关系的生命周期应该有,以及如何/何时应该清理?

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

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

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

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

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

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

  • 1

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

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

  • 0

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

  • 35

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

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

    • 在您的示例中,您正在将对服务定位器的静态引用硬编码到您的类中 . 这个 tightly couples 你的类直接到服务定位器,这又意味着 it won't function without the service locator . 此外,您的单元测试(以及使用该类的任何其他人)也隐式依赖于服务定位器 . 这里似乎没有注意到的一件事是 when using constructor injection you don't need a DI container when unit testing ,它大大简化了你的单元测试(以及开发人员理解它们的能力) . 这是使用构造函数注入获得的已实现的单元测试优势 .

    • 至于为什么构造函数Intellisense很重要,这里的人似乎完全忽略了这一点 . 一个类写一次,但 it may be used in several applications (that is, several DI configurations) . 随着时间的推移,如果您可以查看构造函数定义以了解类's dependencies, rather than looking at the (hopefully up-to-date) documentation or, failing that, going back to the original source code (which might not be handy) to determine what a class'的依赖关系,它会带来好处 . 具有服务定位器的类通常更容易编写,但您不仅要为项目的持续维护支付这种便利的成本 .

    简单明了:一个带有服务定位器的类是 more difficult to reuse ,而不是通过其构造函数接受其依赖关系的类 .

    考虑一下您需要使用LibraryA的服务,其作者决定使用ServiceLocatorA和来自LibraryB的服务,其作者决定使用ServiceLocatorB . 除了在我们的项目中使用2个不同的服务定位器,我们别无选择 . 如果我们没有好的文档,源代码或快速拨号上的作者,那么需要配置多少依赖项就是猜谜游戏 . 如果没有这些选项,我们可能需要使用反编译器来确定依赖关系是什么 . 我们可能需要配置2个完全不同的服务定位器API,根据设计,可能无法简单地包装现有的DI容器 . 根本不可能在两个库之间共享一个依赖关系的实例 . 如果服务定位器实际上并没有与我们需要的服务实际驻留在同一个库中,那么项目的复杂性甚至会更加复杂 - 我们隐含地将其他库引用拖入我们的项目 . 现在考虑使用构造函数注入创建的相同的两个服务 . 添加对LibraryA的引用 . 添加对LibraryB的引用 . 在DI配置中提供依赖关系(通过Intellisense分析所需的内容) . 完成 . Mark Seemann有一个StackOverflow答案,清楚地以图形形式说明了这种好处,这不仅适用于使用其他库中的服务定位器,而且适用于在服务中使用外部默认值时 .

  • 7

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

    这是 Adaptive Code Via C# 的段落:

    “不幸的是,服务定位器有时是不可避免的反模式 . 在某些应用程序类型中 - 特别是Windows Workflow Foundation-基础结构不适合构造函数注入 . 在这些情况下,唯一的选择是使用服务定位器 . 最好不要注入依赖项 . 对于我反对(反)模式的所有讽刺,它比手动构建依赖项要好得多 . 毕竟,它仍然允许那些允许装饰器,适配器的接口提供的所有重要扩展点,以及类似的好处 . “ - 霍尔,加里麦克莱恩 . 通过C#的自适应代码:具有设计模式和SOLID原则的敏捷编码(开发人员参考)(p.309) . 皮尔逊教育 .

  • 0

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

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

    • 隐式得到检查,所以编译"helps" .

    • 您也不需要客户端了解"Locator"或类似机制,因此客户端实际上更独立 .

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

  • 112

    是的,服务定位器是一个反模式它violates encapsulationsolid .

相关问题