服务定位器 - 值得使用吗?

7
我们有一个庞大的解决方案(>100个项目),几乎每种类型都使用服务定位器(示例1)或我们自己的类型字典(示例2)进行实例化。
例如,我们有:
IQuote quote = Registry.Resolve<IQuote>(); 

或者
IQuote quote = Registry.Find<IQuote>(args);

第二个例子会在配置文件中查找具体实例化对象的信息,并使用反射。在跟踪代码时,它使事情变得更加困难 - 因为不清楚使用的是哪种具体类型,所以我们必须多次检查映射,才能学习代码的某个部分。以上面的例子为例,按下F12键:quote.DoSomething()会将您带到接口定义。
实现起来也有点困难 - 我们需要一个接口+具体类+配置映射,而另一种选择只需要一个类。
想想看 - 我并不知道是否曾经有任何东西被“替换”为另一种类型 - 所以尽管我们已经实现了IoC,但我们几乎没有使用它,或者至少很少使用。
所以 - 它真的值得吗?我们实现得不正确/太多了吗?还是我误解了什么?

关于F12仅将您发送到接口定义而不是非常有用的情况,请记住这在某种程度上是Visual Studio的限制,而不仅仅是DI的问题:VS 可以得到改进,以便指向实际调用的方法。显然,VS只能在调试会话期间执行此操作,并且只有在对象的运行时类型确定后才能执行此操作。 - stakx - no longer contributing
这是一个好问题,我真的很想看看一些关于这个问题的意见,但它可能更适合发布在http://programmers.stackexchange.com/。 - Paolo Falabella
@stakx:使用VS自动识别DI类解析可能会起作用,如果在.net中有一个事实上的标准DI实现(我不知道有)。 - Vlad
@Vlad,你误解了我的意思。我并不是说VS可以解析您的DI容器配置,而是调试器可以检查您的“quote”变量的实际类型(如果您在定义和使用该变量的范围内进行调试),然后跳转到该类型的定义,而不是跳转到“quote”的静态类型定义。虽然这个方法有限,但在某些情况下可能会有些帮助,但并不能算得上是一种巨大的改进。 - stakx - no longer contributing
@stakx:哦,我明白了。现在它使用静态分析,因此可以在程序未运行时使用F12。也许应该(或已经)有一些其他命令在运行时检查实际的运行时类型?这将是非常有价值的。 - Vlad
你手头的东西恰恰不是依赖项,而它可以避免依赖项。你手头的是一个工厂类,而不是依赖注入。 - JB Nizet
5个回答

5
你们正在使用的是一个被认为是反模式的服务定位器,原因如下:
  1. 所有单元测试都必须使用服务定位器(没有依赖注入容器的情况下使用DI)
  2. 你将整个架构与服务定位器耦合在一起(在依赖注入中,只有组合根使用依赖注入容器
  3. 你无法立即看到组件的依赖关系(使用依赖注入时有构造函数注入
  4. 你的单元测试变得更加复杂,因为你必须关心你的配置,以确保没有其他测试错误地使用另一个配置(拆卸)。

http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx


4
在我看来,您不需要考虑DI与每个类一起使用。我建议采用以下策略:
  1. 确定模块界限。
  2. 在同一个模块内,尽可能使用具体类。
  3. 对于模块间通信,尽可能使用DI。
模块应该相对细粒度。
有一些常见的地方需要使用DI,通常是可替换的数据源和(不那么经常)算法。使用常识检查是否需要替换某些内容。如果您发现某些内容需要DI或受其影响,请毫不犹豫地进行早期重构。

2
你展示的不是依赖注入,而是可配置的注册表(也是一种模式,但相当不同)。因此,看起来像是一个糟糕的想法和一种误解。
要能够获得某个设计模式的好处,你必须事先了解它们的好处。如果你不知道它们,那么可能就不需要费心了。

+1. 在我发表了同样的评论后,我看到了你的回答。 - JB Nizet
实际上,情况更糟,因为它似乎是服务定位器反模式。正确使用服务定位器并不是坏事。在实现的某个地方使用服务定位器被认为是反模式(似乎在这种情况下是这样)。 - Mare Infinitus

1

如果你进行单元测试,某种形式的依赖注入对于存根或模拟...依赖项至关重要。(单元测试不应该涉及依赖项,如文件系统、数据库、网络等)


0

我认为你把依赖注入的更一般概念和使用 DI 容器混淆了,DI 容器只是实现非常灵活的 DI 的手段。这里有一篇很好的文章解释 DI 的概念。

在你的场景中,我觉得你可以摆脱 DI 容器,但可能仍然希望保留某种形式的 DI,即使只是为了可测试性(正如其他人已经指出的那样)。一个常见的实现模式是通过实现“宽泛”的构造函数来注入依赖项,并在单元测试中使用它们。此外,还将有“较窄”的构造函数,它们将实例化具体类并将它们传递到“宽泛”构造函数中。这些可以在生产代码中使用,直到实际需要交换依赖项为止。

示例:

class MyClass
{   
    // This will be called from production code
    public MyClass() : this(new Foo(), new Bar())
    {}        

    // This will be called from tests and can be used in production code if needed
    public MyClass(IFoo foo, IBar bar)
    {
       //do whatever you want to do in the ctor here
    }
}

请注意:这被称为构造函数注入,在大多数情况下是最好的选择(除非必要时使用属性注入)。 - Mare Infinitus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接