这两种模式似乎都是控制反转原理的实现。也就是说,一个对象不应该知道如何构造它的依赖项。
依赖注入(DI)似乎使用构造函数或setter来“注入”它的依赖项。
使用构造函数注入的例子:
//Foo Needs an IBar
public class Foo
{
private IBar bar;
public Foo(IBar bar)
{
this.bar = bar;
}
//...
}
Service Locator似乎使用了一个“容器”,它连接了它的依赖项并给了foo它的bar。
使用Service Locator的例子:
//Foo Needs an IBar
public class Foo
{
private IBar bar;
public Foo()
{
this.bar = Container.Get<IBar>();
}
//...
}
因为我们的依赖关系只是对象本身,这些依赖关系有依赖关系,依赖关系有更多依赖关系,等等。因此,控制反转容器(或DI容器)诞生了。例如:Castle Windsor, Ninject, Structure Map, Spring等)
但是IOC/DI容器看起来完全像服务定位器。称它为DI容器是一个坏名字吗?IOC/DI容器只是另一种类型的服务定位器吗?当我们有很多依赖时,我们使用依赖注入容器,这是一个细微的差别吗?
A class using constructor DI indicates to consuming code that there are dependencies to be satisfied. If the class uses the SL internally to retrieve such dependencies, the consuming code is not aware of the dependencies. This may on the surface seem better, but it is actually helpful to know of any explicit dependencies. It is better from an architectural view. And when doing testing, you have to know whether a class needs certain dependencies, and configure the SL to provide appropriate fake versions of those dependencies. With DI, just pass in the fakes. Not a huge difference, but it is there.
不过,DI和SL可以一起工作。为常见的依赖项(如设置、记录器等)设置一个中心位置是很有用的。给定一个使用这种deps的类,您可以创建一个接收deps的“真实”构造函数,以及一个从SL检索并转发给“真实”构造函数的默认(无参数)构造函数。
EDIT:当然,当您使用SL时,您将向该组件引入一些耦合。这是具有讽刺意味的,因为这种功能的思想是鼓励抽象和减少耦合。这些关注点是可以平衡的,这取决于您需要在多少地方使用SL。如果按照上面建议的那样做,只是在默认的类构造函数中。
注意:我并不是在回答这个问题。但是我觉得这对于那些对依赖注入模式和服务定位器(反)模式感到困惑的新学习者来说是有用的。
我知道服务定位器(它现在似乎被视为反模式)和依赖注入模式之间的区别,并且可以理解每种模式的具体示例,但我对在构造函数内部显示服务定位器的示例感到困惑(假设我们正在进行构造函数注入)。
“Service Locator”通常既用作模式的名称,也用作该模式中使用的对象的名称(假设也是),以在不使用new操作符的情况下获取对象。现在,同样类型的对象也可以在组合根上用于执行依赖项注入,这就是产生混淆的地方。
需要注意的一点是,您可以在DI构造函数中使用服务定位器对象,但您没有使用“服务定位器模式”。如果将它引用为IoC容器对象,就不会那么令人困惑,因为您可能已经猜到它们本质上做的是相同的事情(如果我错了,请纠正我)。
无论它被称为服务定位器(或仅仅是定位器),还是IoC容器(或仅仅是容器),正如您所猜测的那样,它们都可能引用相同的抽象(如果我说错了,请纠正我)。只是称其为服务定位器表明将服务定位器反模式与依赖注入模式一起使用。
以我之见,将其命名为“定位器”而不是“位置”或“定位”,有时也会导致人们认为文章中的服务定位器指的是服务定位器容器,而不是服务定位器(反)模式,特别是当有一个相关的模式叫做依赖注入而不是依赖注入时。
Martin Fowler说:
使用服务定位器,应用程序类将显式地通过
消息发送给定位器。对于注入,没有显式的请求,
服务出现在应用程序类中——因此是
控制。
简而言之:服务定位器和依赖注入只是依赖反转原理的实现。
重要的原则是“依赖抽象,而不是具象”。这将使你的软件设计“松散耦合”、“可扩展”、“灵活”。
您可以使用最适合您需要的一种。对于拥有庞大代码库的大型应用程序,您最好使用服务定位器,因为依赖注入将需要对代码库进行更多更改。
你可以查看这篇文章:依赖倒置:服务定位器或依赖注入
还有经典的:Martin Fowler的控制容器反转和依赖注入模式
设计可重用类作者:Ralph E. Johnson和Brian Foote
然而,让我大开眼界的是:ASP。NET MVC:解析还是注入?这就是问题所在,迪诺·埃斯波西托著
添加的一个原因是,受到我们上周为MEF项目编写的文档更新的启发(我帮助构建MEF)。
一旦应用程序可能由数千个组件组成,就很难确定任何特定的组件是否可以正确地实例化。通过“正确实例化”,我的意思是在这个基于Foo组件的例子中,IBar的实例和将是可用的,并且提供它的组件将:
有必要的依赖关系,
不涉及任何无效的依赖周期,以及
在MEF的情况下,只提供一个实例。
在你给出的第二个例子中,构造函数去IoC容器检索它的依赖项,你可以测试Foo实例能够正确地实例化应用程序的实际运行时配置的唯一方法是实际构造它。
这在测试时产生了各种尴尬的副作用,因为在运行时可以工作的代码不一定能在测试工具下工作。模拟是不行的,因为我们需要测试的是真正的配置,而不是一些测试时的设置。
这个问题的根源是@Jon已经指出的区别:通过构造函数注入依赖是声明性的,而第二个版本使用命令式的Service Locator模式。
IoC容器,如果使用得当,可以静态地分析应用程序的运行时配置,而无需实际创建任何相关组件的实例。许多流行的容器提供了这方面的一些变化;微软。Composition是针对。net 4.5 web和Metro风格应用程序的MEF版本,在wiki文档中提供了一个compostionassert示例。使用它,你可以编写如下代码:
// Whatever you use at runtime to configure the container
var container = CreateContainer();
CompositionAssert.CanExportSingle<Foo>(container);
(请看这个例子)。
通过在测试时验证应用程序的Composition根,您可以潜在地捕捉到一些错误,否则这些错误可能会在后面的测试过程中遗漏。
希望这是一个有趣的补充,否则这个主题的答案是全面的!
在我的上一个项目中,我两者都用了。
我使用依赖注入进行单元测试。我使用服务定位器来隐藏实现并依赖于我的IoC容器。是的!一旦你使用了IoC容器(Unity, Ninject, Windsor Castle),你就依赖于它了。一旦它过时了,或者由于某种原因你想要交换它,你将/可能需要改变你的实现-至少是组合根。但是服务定位器抽象了这个阶段。
如何不依赖于IoC容器?您要么需要自己包装它(这是一个坏主意),要么使用Service Locator来配置IoC容器。因此,您将告诉Service Locator获取所需的接口,它将调用IoC容器,该容器被配置为检索该接口。
在我的例子中,我使用ServiceLocator,它是一个框架组件。我使用Unity作为IoC容器。如果在未来我需要交换我的IoC容器与Ninject,我所需要做的是配置我的服务定位器使用Ninject而不是Unity。容易迁移。
这里有一篇很棒的文章解释了这种情况;
http://www.johandekoning.nl/index.php/2013/03/03/dont-wrap-your-ioc-container/
郑重声明
//Foo Needs an IBar
public class Foo
{
private IBar bar;
public Foo(IBar bar)
{
this.bar = bar;
}
//...
}
除非你真的需要一个接口(接口被多个类使用),否则你绝对不能使用它。在这种情况下,IBar允许使用实现它的任何服务类。然而,通常,这个接口将由单个类使用。
为什么使用接口不是一个好主意?因为它真的很难调试。
例如,假设实例“bar”失败了,问题:哪个类失败了?我应该修复哪些代码?一个简单的视图,它指向一个界面,我的路到此结束。
相反,如果代码使用硬依赖项,则很容易调试错误。
//Foo Needs an IBar
public class Foo
{
private BarService bar;
public Foo(IBar bar)
{
this.bar = bar;
}
//...
}
如果“酒吧”失败,那么我应该检查和杉木类BarService。
服务定位器和依赖注入都是遵循依赖倒置原则的对象访问模式实现
依赖注入是[static/global]对象访问模式
服务定位器是[动态]对象访问模式
如果你需要处理[动态结构],如[ui树]或任何[分形设计的应用程序],你可能需要服务定位器。
例子:
React的createContext/useContext
提供/注入Vue
angular的提供者
如果您只想从类中获取一个实例,而不关心应用程序的层次结构以及实例在该层次结构中的位置,那么您应该使用DI。
例子:
c# /Java中的注释
如果在运行时之前不知道服务的实际提供者,则使用Service Locator。
当您知道提供该服务的是静态容器时,就使用DI。
服务定位器模式更像是模块级别的依赖提供程序,而依赖注入模式是全局级别的。
当有一个子模块声明了一个服务的依赖关系,该服务应该由它的父模块提供,而不是静态解析类型(单例/瞬态/静态作用域)时,这是非常有用的。
它可以通过DI的作用域注入模式实现,而作用域则由应用程序的模块结构/关系定义。
个人建议:
尽可能使用DI。
如果你必须处理分形结构中的动态/运行时服务解析,请使用Service Locator。
将服务定位符封装为一个有作用域的DI,例如:
通过接口定位服务,而不是通过类/构造函数。
详细信息:https://learn.microsoft.com/zh-cn/dotnet/core/extensions/dependency-injection-guidelines#recommendations