首页 文章

为什么使用依赖注入?

提问于
浏览
478

我想了解dependency injections(DI),我又一次失败了 . 这看起来很傻 . 我的代码从来都不是一团糟;我几乎没有编写虚函数和接口(虽然我曾经在蓝月亮中做过)并且我的所有配置都被神奇地序列化为使用json.net的类(有时使用XML序列化器) .

我不太明白它解决了什么问题 . 它看起来像是一种说法:"hi. When you run into this function, return an object that is of this type and uses these parameters/data."
但是......为什么我会用它呢?注意我也从来不需要使用 object ,但我明白这是为了什么 .

在构建网站或桌面应用程序时,哪些人会使用DI?我可以轻松地提出案例,说明为什么有人可能想在游戏中使用接口/虚拟功能,但在非游戏代码中使用它非常罕见(很少见,我记不起单个实例) .

5 回答

  • 33

    我认为经典的答案是创建一个更加分离的应用程序,它不知道在运行时将使用哪个实现 .

    例如,我们是一家中央支付提供商,与世界各地的许多支付提供商合作 . 但是,当发出请求时,我不知道我打算打电话给哪个付款处理器 . 我可以使用大量的开关案例编写一个类,例如:

    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;
            }
        }
    }
    

    现在想象一下,现在你需要在一个类中维护所有这些代码,因为它没有正确解耦,你可以想象,对于你支持的每个新处理器,你需要创建一个新的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();
    }
    

    **代码不会编译,我知道:)

  • 6

    首先,我想解释一下我为这个答案做出的假设 . 它并不总是如此,但经常是:

    界面是形容词;课程是名词 .

    (实际上,有些界面也是名词,但我想在这里概括一下 . )

    所以,例如接口可以是诸如 IDisposableIEnumerableIPrintable 之类的东西 . 类是这些接口中的一个或多个的实际实现: ListMap 都可以是 IEnumerable 的实现 .

    要明白这一点:通常你的课程相互依赖 . 例如 . 你可以有一个 Database 类访问你的数据库(哈,惊喜!;-)),但你也希望这个类做关于访问数据库的日志 . 假设你有另一个类 Logger ,那么 Database 依赖于 Logger .

    到现在为止还挺好 .

    您可以使用以下行在 Database 类中对此依赖项建模:

    var logger = new Logger();
    

    一切都很好 . 当你意识到你需要一堆 Logger 时,这很好:有时你想要登录控制台,有时你想登录到文件系统,有时候使用TCP / IP和远程登录服务器,等等......

    当然,你不想改变你所有的代码(同时你已经知道了它)并替换所有的代码

    var logger = new Logger();
    

    通过:

    var logger = new TcpLogger();
    

    首先,这不好玩 . 其次,这容易出错 . 第三,对于受过训练的猴子来说,这是一项愚蠢的,重复性的工作 . 所以你会怎么做?

    显然,引入由所有各种 Logger 实现的接口 ICanLog (或类似的)是一个相当不错的主意 . 因此,代码中的第1步是:

    ICanLog logger = new Logger();
    

    现在类型推断不再改变类型,你总是有一个单独的接口来开发 . 下一步是你不想一遍又一遍 new Logger() . 因此,您可以为单个中央工厂类创建新实例,并获得以下代码:

    ICanLog logger = LoggerFactory.Create();
    

    工厂本身决定要创建哪种 Logger . 您的代码不再关心,如果您想更改正在使用的 Logger 类型,您可以更改一次:在工厂内部 .

    当然,现在,您可以概括此工厂,并使其适用于任何类型:

    ICanLog logger = TypeFactory.Create<ICanLog>();
    

    在某个地方,这个TypeFactory需要配置数据,当请求特定的接口类型时,实际的类要实例化,所以你需要一个映射 . 当然,您可以在代码中进行此映射,但类型更改意味着重新编译 . 但是您也可以将此映射放在XML文件中,例如..这允许您甚至在编译时(!)之后更改实际使用的类,这意味着动态,无需重新编译!

    为您提供一个有用的示例:考虑一个不能正常记录的软件,但是当您的客户打电话并因为遇到问题而请求帮助时,您发送给他的所有内容都是更新的XML配置文件,现在他已经已启用日志记录,您的支持人员可以使用日志文件来帮助您的客户 .

    现在,当你稍微更换一下名字时,你最终会得到一个服务定位器的简单实现,这是控制反转的两种模式之一(从那以后)你可以控制谁决定要实例化的确切类 .

    总而言之,这减少了代码中的依赖关系,但现在所有代码都依赖于中央单一服务定位器 .

    依赖注入现在是这一行的下一步:只需摆脱对服务定位器的这种单一依赖:代替各种类向服务定位器询问特定接口的实现,你 - 再次 - 恢复对谁实例化什么的控制 .

    使用依赖注入, Database 类现在有一个构造函数,它需要一个类型为 ICanLog 的参数:

    public Database(ICanLog logger) { ... }
    

    现在你的数据库总是有一个 Logger 可供使用,但它不知道这个 Logger 的来源 .

    这就是DI框架发挥作用的地方:您再次配置映射,然后让您的DI框架为您实例化您的应用程序 . 由于 Application 类需要 ICanPersistData 实现,因此会注入 Database 的实例 - 但为此必须首先创建为 ICanLog 配置的 Logger 类型的实例 . 等等 ...

    因此,简而言之:依赖注入是如何在代码中删除依赖关系的两种方法之一 . 它对于编译后的配置更改非常有用,对于单元测试来说它是一件好事(因为它可以很容易地注入存根和/或模拟) .

    实际上,如果没有服务定位器,有些事情是你无法做到的(例如,如果你事先不知道你需要多少个特定接口的实例:一个DI框架每个参数总是只注入一个实例,但你可以调用当然,循环内的服务定位器,因此大多数情况下每个DI框架也提供服务定位器 .

    但基本上就是这样 .

    希望有所帮助 .

    PS:我在这里描述的是一种称为构造函数注入的技术,还有属性注入,其中没有构造函数参数,但属性用于定义和解析依赖项 . 将属性注入视为可选依赖项,将构造函数注入视为必需依赖项 . 但对此的讨论超出了这个问题的范围 .

  • 466

    正如其他答案所述,依赖注入是一种在使用它的类之外创建依赖项的方法 . 你从外面注入它们,并从你的 class 内部控制他们的创造 . 这也是依赖注入是Inversion of control(IoC)原则实现的原因 .

    IoC是原则,其中DI是模式 . 根据我的经验,您可能“需要多个 Logger ”的原因从未真正得到满足,但实际原因是,无论何时测试某些东西,您确实需要它 . 一个例子:

    My Feature:

    当我查看报价时,我想标记我自动查看它,以便我不会忘记这样做 .

    您可以像这样测试:

    [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);
    }
    

    像这样,您通过注入依赖项(获取当前时间)来应用"inversion of control"原则 . 这样做的主要原因是为了更容易进行隔离单元测试,还有其他方法可以做到这一点 . 例如,这里的接口和类是不必要的,因为在C#函数中可以作为变量传递,因此您可以使用 Func<DateTime> 来实现相同的接口而不是接口 . 或者,如果采用动态方法,则只传递具有等效方法(duck typing)的任何对象,并且根本不需要接口 .

    您几乎不需要多个 Logger . 尽管如此,依赖注入对于静态类型代码(如Java或C#)至关重要 .

    And... 还应该注意,如果一个对象的所有依赖项都可用,那么它只能在运行时正确地实现其目的,因此在设置属性注入时没有多大用处 . 在我看来,在调用构造函数时应该满足所有依赖项,因此构造函数注入是最佳选择 .

    我希望有所帮助 .

  • 12

    我认为很多时候人们对依赖注入和依赖注入框架(或者通常称为容器)之间的区别感到困惑 .

    依赖注入是一个非常简单的概念 . 而不是这个代码:

    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();
    }
    

    And that's it. 说真的 . 这给你带来了很多好处 . 两个重要的是能力从中心位置( Main() 函数)控制功能,而不是在整个程序中传播它,并且能够更容易地隔离测试每个类(因为你可以将模拟或其他伪造的对象传递到它的构造函数而不是实际值) .

    当然,缺点是你现在有一个知道程序使用的所有类的超级函数 . 这就是DI框架可以提供的帮助 . 但是,如果您无法理解为什么这种方法很有 Value ,我建议首先从手动依赖注入开始,这样您就可以更好地了解各种框架可以为您做些什么 .

  • 786

    使用DI的主要原因是您希望将实施知识的责任放在知识所在的位置 . DI的概念非常符合界面封装和设计 . 如果前端从后端询问某些数据,那么前端后端如何解决该问题并不重要 . 这取决于requesthandler .

    这在OOP中已经很常见了很长时间 . 很多时候创建代码片段如:

    I_Dosomething x = new Impl_Dosomething();
    

    缺点是实现类仍然是硬编码的,因此前端具有使用实现的知识 . DI通过接口进一步采用设计,前端唯一需要知道的是接口的知识 . 在DYI和DI之间是服务定位器的模式,因为前端必须提供密钥(存在于服务定位器的注册表中)以使其请求得到解决 . 服务定位器示例:

    I_Dosomething x = ServiceLocator.returnDoing(String pKey);
    

    DI示例:

    I_Dosomething x = DIContainer.returnThat();
    

    DI的一个要求是容器必须能够找出哪个类是哪个接口的实现 . 因此,DI容器需要强类型设计,并且每个接口同时只需要一个实现 . 如果您需要同时实现更多接口(如计算器),则需要服务定位器或工厂设计模式 .

    D(b)I:依赖注入和接口设计 . 这种限制虽然不是一个很大的实际问题 . 使用D(b)I的好处是它服务于客户端和提供者之间的通信 . 界面是对象或一组行为的透视图 . 后者在这里至关重要 .

    我更喜欢在编码时与D(b)I一起管理服务 Contract . 他们应该一起去 . 在我看来,使用D(b)I作为没有组织管理服务 Contract 的技术解决方案并不是非常有益,因为DI只是一个额外的封装层 . 但是当你可以将它与组织管理一起使用时,你可以真正地利用我提供的组织原则D(b) . 从长远来看,它可以帮助您与客户和其他技术部门 Build 关于测试,版本控制和替代方案开发等主题的沟通 . 当你有一个隐藏的接口,就像在硬编码的类中一样,那么随着时间的推移,当你使用D(b)I明确表达时,它的可通信性要小得多 . 这一切都归结为维护,这是随着时间的推移,而不是一次 . :-)

相关问题