我正在尝试理解依赖注射(DI),并再一次失败了.这看起来很傻.我的代码从来都不是一团糟; 我几乎没有编写虚函数和接口(虽然我曾经在蓝月亮中做过)并且我的所有配置都被神奇地序列化为使用json.net的类(有时使用XML序列化器).
我不太明白它解决了什么问题.它看起来像是一种说法:"嗨.当你遇到这个函数时,返回一个这种类型的对象并使用这些参数/数据."
但是......为什么我会用它呢?注意我从来不需要使用object,但我明白这是什么.
在构建网站或桌面应用程序时,哪些人会使用DI?我可以轻松地提出案例,为什么有人可能想在游戏中使用接口/虚拟功能,但在非游戏代码中使用它非常罕见(很少见,我记不起单个实例).
Gol*_*den 829
首先,我想解释一下我为这个答案做出的假设.它并不总是如此,但经常是:
界面是形容词; 课程是名词.
(实际上,有些名词也是名词,但我想在这里概括一下.)
因此,例如,一个接口可以是这样的东西IDisposable,IEnumerable或IPrintable.类是这些接口中的一个或多个的实际实现:List或者Map两者都可以是实现IEnumerable.
要明白这一点:通常你的课程相互依赖.例如,你可以有一个Database访问你的数据库的类(哈,惊喜!;-)),但你也希望这个类做关于访问数据库的日志.假设你有另一个类Logger,那么Database依赖于Logger.
到现在为止还挺好.
您可以Database使用以下行在类中对此依赖项建模:
var logger = new Logger();
Run Code Online (Sandbox Code Playgroud)
一切都很好.当你意识到你需要一堆记录器时,这是很好的:有时你想要登录到控制台,有时你想登录到文件系统,有时候使用TCP/IP和远程登录服务器,等等......
当然,你不要想改变所有的代码(同时你拥有它gazillions)和替换所有行
var logger = new Logger();
Run Code Online (Sandbox Code Playgroud)
通过:
var logger = new TcpLogger();
Run Code Online (Sandbox Code Playgroud)
首先,这不好玩.其次,这容易出错.第三,对于训练有素的猴子来说,这是一项愚蠢的,重复性的工作.所以你会怎么做?
显然,引入ICanLog由所有各种记录器实现的接口(或类似的)是一个非常好的主意.因此,代码中的第1步是:
ICanLog logger = new Logger();
Run Code Online (Sandbox Code Playgroud)
现在类型推断不再改变类型,你总是有一个单独的接口来开发.下一步是你不想new Logger()一遍又一遍.因此,您可以为单个中央工厂类创建新实例,并获得以下代码:
ICanLog logger = LoggerFactory.Create();
Run Code Online (Sandbox Code Playgroud)
工厂本身决定要创建哪种记录器.您的代码不再关心,如果您想更改正在使用的记录器类型,您可以更改一次:在工厂内部.
当然,现在您可以概括这个工厂,并使其适用于任何类型:
ICanLog logger = TypeFactory.Create<ICanLog>();
Run Code Online (Sandbox Code Playgroud)
在某个地方,这个TypeFactory需要配置数据,当请求特定的接口类型时,实际的类要实例化,所以你需要一个映射.当然,您可以在代码中进行此映射,但是类型更改意味着重新编译.但您也可以将此映射放在XML文件中,例如.这允许您甚至在编译时(!)之后更改实际使用的类,这意味着动态地,无需重新编译!
为您提供一个有用的示例:想想一个不能正常登录的软件,但是当您的客户打电话并因为遇到问题而请求帮助时,您发送给他的只是一个更新的XML配置文件,现在他已经已启用日志记录,您的支持人员可以使用日志文件来帮助您的客户.
现在,当您稍微更换名称时,最终会得到一个服务定位器的简单实现,这是控制反转的两种模式之一(因为您可以控制谁决定要实例化的确切类).
总而言之,这减少了代码中的依赖关系,但现在所有代码都依赖于中央单一服务定位器.
依赖注入现在是这一行的下一步:只需要去掉服务定位器的这个单一依赖:代替各种类询问服务定位器是否有特定接口的实现,你再一次 - 恢复对谁实例化什么的控制.
使用依赖注入,您的Database类现在有一个构造函数,它需要一个类型的参数ICanLog:
public Database(ICanLog logger) { ... }
Run Code Online (Sandbox Code Playgroud)
现在你的数据库总是有一个记录器可供使用,但它不知道这个记录器的来源.
这就是DI框架发挥作用的地方:您再次配置映射,然后让您的DI框架为您实例化您的应用程序.由于Application类需要ICanPersistData实现,因此Database会注入一个实例- 但为此必须首先创建一个配置的logger实例ICanLog.等等 ...
因此,简而言之:依赖注入是如何在代码中删除依赖关系的两种方法之一.它对于编译后的配置更改非常有用,对于单元测试来说它是一件好事(因为它可以很容易地注入存根和/或模拟).
在实践中,有些事情没有服务定位器就无法做到(例如,如果你事先不知道你需要多少个特定接口的实例:一个DI框架总是每个参数只注入一个实例,但你可以调用当然,循环内的服务定位器),因此大多数情况下每个DI框架也提供服务定位器.
但基本上就是这样.
希望有所帮助.
PS:我在这里描述的是一种称为构造函数注入的技术,还有属性注入,其中没有构造函数参数,但属性用于定义和解析依赖项.将属性注入视为可选依赖项,将构造函数注入视为必需依赖项.但对此的讨论超出了这个问题的范围.
Dan*_*den 491
我想很多时候人们对依赖注入和依赖注入框架(或者通常称为容器)之间的区别感到困惑.
依赖注入是一个非常简单的概念.而不是这段代码:
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();
}
Run Code Online (Sandbox Code Playgroud)
你写这样的代码:
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();
}
Run Code Online (Sandbox Code Playgroud)
就是这样.认真.这给你带来了很多好处.两个重要的功能是能够从中心位置(Main()函数)控制功能,而不是在整个程序中传播它,并且能够更容易地单独测试每个类(因为您可以将模拟或其他伪造的对象传递到其构造函数中一个真正的价值).
当然,缺点是你现在有一个知道你的程序使用的所有类的超级函数.这就是DI框架可以提供的帮助.但如果您无法理解为什么这种方法很有价值,我建议首先从手动依赖注入开始,这样您就可以更好地了解那里的各种框架可以为您做些什么.
ces*_*sor 35
正如其他答案所述,依赖注入是一种在使用它的类之外创建依赖项的方法.你从外面注入它们,并从你的班级内部控制他们的创造.这也是依赖注入是控制反转(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));
}
Run Code Online (Sandbox Code Playgroud)
所以在OfferWeasel它的某个地方,它构建了一个像这样的商品对象:
public class OfferWeasel
{
public Offer Create(Formdata formdata)
{
var offer = new Offer();
offer.LastUpdated = DateTime.Now;
return offer;
}
}
Run Code Online (Sandbox Code Playgroud)
这里的问题是,这个测试很可能总是失败,因为正在设置的日期将与被声明的日期不同,即使您只是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;
}
}
Run Code Online (Sandbox Code Playgroud)
接口是抽象.一个是真实的东西,另一个允许你假装需要它的时间.然后可以像这样更改测试:
[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);
}
Run Code Online (Sandbox Code Playgroud)
像这样,你通过注入一个依赖(获取当前时间)来应用"控制反转"原理.这样做的主要原因是为了更容易进行隔离单元测试,还有其他方法可以做到这一点.例如,这里的接口和类是不必要的,因为在C#函数中可以作为变量传递,因此您可以使用a Func<DateTime>来实现相同的接口而不是接口 .或者,如果采用动态方法,则只传递具有等效方法的任何对象(鸭子类型),并且根本不需要接口.
您几乎不需要多个记录器.尽管如此,依赖注入对于静态类型代码(如Java或C#)至关重要.
并且...... 还应该注意,如果一个对象的所有依赖项都可用,那么它只能在运行时正确地实现其目的,因此在设置属性注入时没有多大用处.在我看来,在调用构造函数时应该满足所有依赖项,因此构造函数注入是最佳选择.
我希望有所帮助.
Ita*_*agi 13
我认为经典的答案是创建一个更加分离的应用程序,它不知道在运行时将使用哪个实现.
例如,我们是一家中央支付提供商,与世界各地的许多支付提供商合作.但是,当提出请求时,我不知道我要打电话给哪个支付处理器.我可以使用大量的开关案例编写一个类,例如:
class PaymentProcessor{
private String type;
public PaymentProcessor(String type){
this.type = type;
}
public void authorize(){
if (type.equals(Consts.PAYPAL)){
// Do this;
}
else if(type.equals(Consts.OTHER_PROCESSOR)){
// Do that;
}
}
}
Run Code Online (Sandbox Code Playgroud)
现在想象一下,现在你需要在一个类中维护所有这些代码,因为它没有正确解耦,你可以想象,对于你支持的每个新处理器,你需要创建一个新的if // switch案例每个方法,这只会变得更复杂,但是,通过使用依赖注入(或控制反转 - 因为它有时被称为,意味着无论谁控制程序的运行只在运行时知道,而不是复杂),你可以实现一些东西非常整洁和可维护.
class PaypalProcessor implements PaymentProcessor{
public void authorize(){
// Do PayPal authorization
}
}
class OtherProcessor implements PaymentProcessor{
public void authorize(){
// Do other processor authorization
}
}
class PaymentFactory{
public static PaymentProcessor create(String type){
switch(type){
case Consts.PAYPAL;
return new PaypalProcessor();
case Consts.OTHER_PROCESSOR;
return new OtherProcessor();
}
}
}
interface PaymentProcessor{
void authorize();
}
Run Code Online (Sandbox Code Playgroud)
**代码不会编译,我知道:)
使用DI的主要原因是您希望将实施知识的责任放在知识所在的位置.DI的概念非常符合界面封装和设计.如果前端从后端询问某些数据,那么前端后端如何解决该问题并不重要.这取决于requesthandler.
这在OOP中已经很常见了很长时间.很多时候创建代码片段如:
I_Dosomething x = new Impl_Dosomething();
Run Code Online (Sandbox Code Playgroud)
缺点是实现类仍然是硬编码的,因此前端具有使用实现的知识.DI通过接口进一步采用设计,前端唯一需要知道的是接口的知识.在DYI和DI之间是服务定位器的模式,因为前端必须提供密钥(存在于服务定位器的注册表中)以使其请求得到解决.服务定位器示例:
I_Dosomething x = ServiceLocator.returnDoing(String pKey);
Run Code Online (Sandbox Code Playgroud)
DI例子:
I_Dosomething x = DIContainer.returnThat();
Run Code Online (Sandbox Code Playgroud)
DI的一个要求是容器必须能够找出哪个类是哪个接口的实现.因此,DI容器需要强类型设计,并且每个接口同时只需要一个实现.如果您需要同时实现更多接口(如计算器),则需要服务定位器或工厂设计模式.
D(b)I:依赖注入和接口设计.这种限制虽然不是一个很大的实际问题.使用D(b)I的好处是它服务于客户端和提供者之间的通信.界面是对象或一组行为的透视图.后者在这里至关重要.
我更喜欢在编码时与D(b)I一起管理服务合同.他们应该一起去.在我的观点中,使用D(b)I作为技术解决方案而没有组织管理服务合同并不是非常有益,因为DI只是一个额外的封装层.但是当你可以将它与组织管理一起使用时,你可以真正地利用我提供的组织原则D(b).从长远来看,它可以帮助您与客户和其他技术部门建立联系,包括测试,版本控制和替代方案的开发.当你有一个隐藏的接口,就像在硬编码的类中一样,那么随着时间的推移,当你使用D(b)I将其显式化时,它的可通信性要小得多.这一切都归结为维护,这是随着时间的推移,而不是一次.:-)
| 归档时间: |
|
| 查看次数: |
227659 次 |
| 最近记录: |