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