首先,我想解释一下我为这个答案所做的一个假设。这并不总是正确的,但经常是这样的:
接口是形容词;类是名词。
(实际上,有些接口也是名词,但我想在这里概括一下。)
例如,一个接口可能是IDisposable, IEnumerable或IPrintable。类是这些接口中的一个或多个的实际实现:List或Map都可以是IEnumerable的实现。
要点在于:通常你的类是相互依赖的。例如,你可以有一个数据库类来访问你的数据库(哈,惊喜!;-)),但是您还希望这个类执行关于访问数据库的日志记录。假设您有另一个类Logger,那么Database就依赖于Logger。
到目前为止,一切顺利。
你可以在你的数据库类中建模这个依赖:
var logger = new Logger();
一切都很好。直到有一天您意识到您需要一堆日志记录程序:有时您想要记录到控制台,有时想要记录到文件系统,有时使用TCP/IP和远程日志服务器,等等……
当然,你不希望改变你所有的代码(同时你有无数的代码)并替换所有的行
var logger = new Logger();
by:
var logger = new TcpLogger();
首先,这一点都不好玩。其次,这很容易出错。第三,这对一只训练有素的猴子来说是愚蠢而重复的工作。你会怎么做呢?
显然,引入一个接口ICanLog(或类似的)是一个很好的主意,可以由各种各样的日志记录器实现。代码中的第一步是:
ICanLog logger = new Logger();
现在类型推断不再改变类型,您总是有一个单独的接口来开发。下一步是不希望反复使用新的Logger()。所以你把创建新实例的可靠性放在一个单一的中央工厂类中,你会得到这样的代码:
ICanLog logger = LoggerFactory.Create();
工厂自己决定创建哪种记录器。您的代码不再关心这个问题,如果您想要更改所使用的记录器的类型,只需更改一次:在工厂内部。
当然,现在你可以泛化这个工厂,让它适用于任何类型:
ICanLog logger = TypeFactory.Create<ICanLog>();
当请求特定接口类型时,这个TypeFactory需要实例化实际类的配置数据,因此需要映射。当然,您可以在代码中进行这种映射,但是类型更改意味着重新编译。但是你也可以把这个映射放在一个XML文件中,例如。这允许您在编译之后更改实际使用的类(!),这意味着动态地,而不需要重新编译!
举一个有用的例子:假设一个软件不能正常记录日志,但是当客户因为遇到问题而打电话并寻求帮助时,您发送给他的只是一个更新的XML配置文件,现在他启用了日志记录,您的支持可以使用日志文件来帮助客户。
现在,稍微替换一下名称,就得到了Service Locator的简单实现,它是控制反转的两种模式之一(因为您反转了谁决定具体实例化什么类的控制)。
总之,这减少了代码中的依赖关系,但现在所有的代码都依赖于中心的单一服务定位器。
依赖注入现在是这一行的下一步:只需摆脱对服务定位器的单个依赖:而不是各种类向服务定位器请求特定接口的实现,您—再一次—恢复了谁实例化什么的控制。
使用依赖注入,你的数据库类现在有一个构造函数,它需要一个类型为ICanLog的参数:
public Database(ICanLog logger) { ... }
现在您的数据库总是有一个记录器可以使用,但是它不知道这个记录器来自哪里。
这就是DI框架发挥作用的地方:您再次配置映射,然后要求DI框架为您实例化应用程序。由于Application类需要一个ICanPersistData实现,因此注入了一个数据库实例——但为此它必须首先创建一个为ICanLog配置的记录器类型的实例。等等……
因此,长话短说:依赖注入是在代码中删除依赖的两种方法之一。它对于编译后的配置更改非常有用,对于单元测试也非常有用(因为它使注入存根和/或模拟变得非常容易)。
在实践中,如果没有服务定位器,有些事情是不能做的(例如,如果您事先不知道特定接口需要多少实例:DI框架总是每个参数只注入一个实例,但当然,您可以在循环中调用服务定位器),因此大多数情况下,每个DI框架也提供一个服务定位器。
但基本上,就是这样。
附注:我在这里描述的是一种称为构造函数注入的技术,还有属性注入,其中没有构造函数参数,但属性被用于定义和解析依赖关系。可以将属性注入视为可选依赖项,将构造函数注入视为强制依赖项。但是关于这个问题的讨论超出了这个问题的范围。