我试图理解依赖注入(DI),但我又一次失败了。只是看起来很傻。我的代码从不乱七八糟;我几乎不写虚函数和接口(尽管我曾经做过一次),而且我的所有配置都使用 json.net 神奇地序列化为一个类(有时使用 XML 序列化程序)。
我不太明白它解决了什么问题。它看起来像是在说:“嗨。当你遇到这个函数时,返回一个这种类型的对象并使用这些参数/数据。” 但是…我为什么要使用它?请注意,我也从未需要使用object过,但我了解它的用途。
object
在构建使用 DI 的网站或桌面应用程序时,有哪些实际情况?我可以很容易地想出一些案例来解释为什么有人可能想在游戏中使用接口/虚拟函数,但是在非游戏代码中使用它是极其罕见的(非常罕见以至于我记不起一个实例)。
首先,我想解释一下我为这个答案所做的假设。这并不总是正确的,但经常是:
接口是形容词;类是名词。
(其实也有名词的接口,但我想在这里概括一下。)
因此,例如,接口可能是诸如IDisposable、IEnumerable或之类的东西IPrintable。一个类是这些接口中的一个或多个的实际实现:List或者Map两者都可以是IEnumerable.
IDisposable
IEnumerable
IPrintable
List
Map
要明白这一点:您的课程通常相互依赖。例如,您可以有一个Database访问您的数据库的类(哈哈,惊喜!;-)),但您还希望该类记录有关访问数据库的日志。假设您有另一个类Logger,然后Database依赖于Logger.
Database
Logger
到目前为止,一切都很好。
您可以Database使用以下行在您的类中对此依赖项进行建模:
var logger = new Logger();
一切都很好。当你意识到你需要一堆记录器时,这很好:有时你想记录到控制台,有时记录到文件系统,有时使用 TCP/IP 和远程记录服务器,等等......
当然,您 不想 更改所有代码(同时您拥有大量代码)并替换所有行
经过:
var logger = new TcpLogger();
首先,这不好玩。其次,这是容易出错的。第三,对于受过训练的猴子来说,这是愚蠢的、重复的工作。所以你会怎么做?
显然,引入一个ICanLog由所有各种记录器实现的接口(或类似接口)是一个非常好的主意。因此,代码中的第 1 步是您执行以下操作:
ICanLog
ICanLog logger = new 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. 等等 …
Application
ICanPersistData
所以,长话短说:依赖注入是删除代码中依赖的两种方法之一。它对于编译时之后的配置更改非常有用,并且对于单元测试来说是一件好事(因为它使得注入存根和/或模拟变得非常容易)。
在实践中,有些事情没有服务定位器就无法完成(例如,如果您事先不知道特定接口需要多少个实例:DI 框架总是为每个参数注入一个实例,但您可以调用一个循环内的服务定位器,当然),因此大多数情况下每个 DI 框架也提供一个服务定位器。
但基本上,就是这样。
PS:我在这里描述的是一种称为 构造函数注入 的技术,还有 属性注入 ,其中不是构造函数参数,而是用于定义和解决依赖关系的属性。将属性注入视为可选依赖项,将构造函数注入视为强制依赖项。但是对此的讨论超出了这个问题的范围。