为什么要使用依赖注入?
我试图理解依赖注入(DI),并再次失败。 它看起来很愚蠢。 我的代码从来都不是一团糟; 我几乎不写虚拟函数和接口(虽然我只做了一次蓝月亮),并且所有配置都使用json.net(有时使用XML序列化程序)奇迹般地序列化为一个类。
我不太明白它解决了什么问题。 它看起来像是一种说法:“嗨,当你遇到这个函数时,返回一个这种类型的对象并使用这些参数/数据。”
但是...为什么我会使用它? 注意我从来不需要使用object
,但我明白这是什么意思。
在建立一个使用DI的网站或桌面应用程序时,有哪些真实情况? 我可以很容易地想出为什么有人可能想要在游戏中使用接口/虚函数的情况,但是它非常罕见(很少有我不记得单个实例)在非游戏代码中使用它。
首先,我想解释一下我为这个答案所做的一个假设。 这并非总是如此,但经常是这样:
接口是形容词; 类是名词。
(实际上,也有名词的界面,但我想在此概括一下。)
所以,例如一个接口可能是诸如IDisposable
, IEnumerable
或IPrintable
。 一个类是一个或多个这些接口的实际实现: List
或Map
都可以是IEnumerable
实现。
为了得到这个观点:通常你的课程相互依赖。 例如你可以有一个访问数据库的Database
类(hah,surprise!;-)),但是你也希望这个类能够访问数据库。 假设你有另一个类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()
使用new Logger()
。 因此,您将可靠性创建为单个中央工厂类的新实例,并获得如下代码:
ICanLog logger = LoggerFactory.Create();
工厂自己决定创建什么样的记录器。 您的代码不再在意,如果您想更改正在使用的记录器的类型,请将其更改一次:在工厂内部。
现在,当然,你可以概括这个工厂,并使其适用于任何类型:
ICanLog logger = TypeFactory.Create<ICanLog>();
这个TypeFactory需要配置数据,当请求一个特定的接口类型时,需要实际的类来实例化,所以你需要一个映射。 当然,你可以在你的代码中做这种映射,但是类型改变意味着重新编译。 但是你也可以把这个映射放在一个XML文件中,例如。 这使您可以在编译时(!)之后更改实际使用的类,这意味着可以动态地重新编译!
给你一个有用的例子:想想一个没有正常记录的软件,但是当你的客户打电话问他需要帮助,因为他有问题,你发给他的只是一个更新的XML配置文件,现在他已经日志启用,并且您的支持可以使用日志文件来帮助您的客户。
现在,当你替换一些名字时,你最终得到了一个服务定位器的简单实现,它是反转控制的两种模式之一(因为你反过来控制谁决定实例化什么类)。
总而言之,这可以减少代码中的依赖关系,但现在所有的代码都依赖于中央的单一服务定位器。
依赖注入现在是这一行的下一个步骤:只要摆脱对服务定位器的这种单一依赖关系:而不是各种类向服务定位器询问特定接口的实现,而是再次 - 控制谁实例化什么。
通过依赖注入,你的Database
类现在有一个构造函数,它需要一个ICanLog
类型的参数:
public Database(ICanLog logger) { ... }
现在你的数据库总是有一个记录器来使用,但它不知道这个记录器来自哪里。
这就是DI框架发挥作用的地方:您再次配置您的映射,然后让您的DI框架为您实例化您的应用程序。 由于Application
类需要一个ICanPersistData
实现,因此将注入一个Database
实例 - 但为此,它必须首先创建一个为ICanLog
配置的记录器实例。 等等 ...
所以,简而言之,简单来说就是:依赖注入是如何去除代码中的依赖关系的两种方法之一。 这对于编译后的配置更改非常有用,对于单元测试来说这是一件好事(因为它可以非常容易地注入存根和/或模拟)。
在实践中,没有服务定位器是不能做的事情(例如,如果您事先不知道需要多少个实例需要特定的接口:DI框架每个参数总是只注入一个实例,但您可以调用当然是一个循环内的服务定位器),因此大多数时候每个DI框架还提供一个服务定位器。
但基本上就是这样。
希望有所帮助。
PS:我在这里描述的是一种称为构造函数注入的技术,还有属性注入,其中不是构造函数参数,而是用于定义和解决依赖关系的属性。 将属性注入视为可选的依赖项,将构造函数注入视为必需的依赖项。 但是关于这个的讨论超出了这个问题的范围。
我认为很多时候人们对依赖注入和依赖注入框架(或者它经常被称为的容器)之间的区别感到困惑。
依赖注入是一个非常简单的概念。 代替这个代码:
public class A {
private B b;
public A() {
this.b = new B(); // A *depends on* B
}
public void DoSomeStuff() {
// Do something with B here
}
}
public static void Main(string[] args) {
A a = new A();
a.DoSomeStuff();
}
你写这样的代码:
public class A {
private B b;
public A(B b) { // A now takes its dependencies as arguments
this.b = b; // look ma, no "new"!
}
public void DoSomeStuff() {
// Do something with B here
}
}
public static void Main(string[] args) {
B b = new B(); // B is constructed here instead
A a = new A(b);
a.DoSomeStuff();
}
就是这样。 认真。 这给你很多好处。 其中两个重要的功能是从中心位置( Main()
函数)控制功能,而不是将其分散到整个程序中,并且可以更轻松地单独测试每个类(因为您可以将模拟对象或其他伪造对象传入它的构造函数而不是真正的价值)。
当然,缺点是你现在有一个超级函数可以知道程序使用的所有类。 这是DI框架可以提供帮助的。 但是,如果您无法理解为什么这种方法很有价值,那么我建议首先从手动依赖注入入手,这样您可以更好地理解那里的各种框架可以为您做些什么。
正如其他答案所述,依赖注入是一种在使用它的类之外创建依赖关系的方法。 你从外面注入它们,并且从你的课堂内部控制他们的创作。 这也是为什么依赖注入是控制反转(IoC)原理的一个实现。
IoC是原则,其中DI是模式。 就我的经验而言,你可能“需要多个记录器”的原因从来没有真正满足过,但实际的原因是,当你测试某些东西时,你确实需要它。 一个例子:
我的特色:
当我看到一个提议时,我想标记我自动查看它,这样我就不会忘记这么做。
你可能会这样测试它:
[Test]
public void ShouldUpdateTimeStamp
{
// Arrange
var formdata = { . . . }
// System under Test
var weasel = new OfferWeasel();
// Act
var offer = weasel.Create(formdata)
// Assert
offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}
因此,在OfferWeasel
某个地方,它会为您创建一个对象,如下所示:
public class OfferWeasel
{
public Offer Create(Formdata formdata)
{
var offer = new Offer();
offer.LastUpdated = DateTime.Now;
return offer;
}
}
这里的问题是,这个测试很可能总是失败,因为被设置的日期与被声明的日期不同,即使你只是把DateTime.Now
放在测试代码中,它可能会被关闭几毫秒并因此总是失败。 现在更好的解决方案是为此创建一个接口,它允许您控制将设置的时间:
public interface IGotTheTime
{
DateTime Now {get;}
}
public class CannedTime : IGotTheTime
{
public DateTime Now {get; set;}
}
public class ActualTime : IGotTheTime
{
public DateTime Now {get { return DateTime.Now; }}
}
public class OfferWeasel
{
private readonly IGotTheTime _time;
public OfferWeasel(IGotTheTime time)
{
_time = time;
}
public Offer Create(Formdata formdata)
{
var offer = new Offer();
offer.LastUpdated = _time.Now;
return offer;
}
}
接口是抽象的。 一个是真正的事情,另一个允许你在需要的地方假装一段时间。 测试可以像这样改变:
[Test]
public void ShouldUpdateTimeStamp
{
// Arrange
var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
var formdata = { . . . }
var time = new CannedTime { Now = date };
// System under test
var weasel= new OfferWeasel(time);
// Act
var offer = weasel.Create(formdata)
// Assert
offer.LastUpdated.Should().Be(date);
}
像这样,你通过注入依赖(获取当前时间)来应用“控制反转”原理。 这样做的主要原因是为了更容易进行单元测试,还有其他方法可以做到这一点。 例如,这里的接口和类是不必要的,因为在C#中函数可以作为变量传递,所以可以使用Func<DateTime>
来实现相同的接口而不是接口。 或者,如果采取动态方法,您只需传递任何具有等效方法的对象(鸭子打字),并且根本不需要接口。
你几乎不需要多于一个记录器。 尽管如此,依赖注入对于静态类型代码(例如Java或C#)而言至关重要。
还有......还应该注意的是,一个对象只能在运行时正确实现其目的,如果它的所有依赖关系都可用的话,那么在设置属性注入方面就没有多大用处。 在我看来,当构造函数被调用时,所有的依赖关系都应该被满足,所以构造函数注入是一件好事。
我希望有所帮助。
链接地址: http://www.djcxy.com/p/2325.html