代码之家  ›  专栏  ›  技术社区  ›  Epaga Alex Reynolds

使用依赖注入的缺点是什么?[闭门]

  •  327
  • Epaga Alex Reynolds  · 技术社区  · 16 年前

    我正在尝试将DI作为一种模式引入工作中,我们的一位主要开发人员想知道:如果有的话,它是什么 缺点 使用依赖注入模式?

    请注意,我在这里寻找一个——如果可能的话——详尽的列表,而不是关于这个主题的主观讨论。


    澄清 :我说的是依赖注入 图案 (见 this article 作者:马丁·福勒), 一个特定的框架,无论是基于XML(如Spring)还是基于代码(如Guice),还是“自滚式”。


    编辑 当前位置正在进行进一步的讨论/咆哮/辩论 /r/programming 在这里

    19 回复  |  直到 15 年前
        1
  •  223
  •   Joe    15 年前

    有两点:

    • DI增加了复杂性,通常是通过增加类的数量来实现的,因为职责被分离得更多,这并不总是有益的
    • 您的代码将(在某种程度上)与您使用的依赖注入框架相耦合(或者更一般地说,与您决定如何实现DI模式相耦合)
    • 执行类型解析的DI容器或方法通常会产生轻微的运行时惩罚(可以忽略不计,但确实存在)

    通常,解耦的好处是使每个任务更易于阅读和理解,但会增加编排更复杂任务的复杂性。

        2
  •  194
  •   Robert Harvey    11 年前

    与面向对象编程、样式规则和几乎所有其他方面经常遇到的基本问题相同。有可能——事实上,这是很常见的——做太多的抽象,添加太多的间接性,并且通常过度地在错误的地方应用好的技术。

    你应用的每一种模式或其他构造都会带来复杂性。抽象和间接分散信息,有时将不相关的细节移到一边,但有时同样会使人们更难准确地理解发生了什么。你应用的每一条规则都会带来僵化,排除了可能是最佳方法的选项。

    关键是要编写能够完成这项工作、健壮、可读和可维护的代码。你是一个软件开发人员,而不是一个象牙塔建设者。

    相关链接

    http://thedailywtf.com/Articles/The_Inner-Platform_Effect.aspx

    http://www.joelonsoftware.com/articles/fog0000000018.html


    依赖注入的最简单形式(不要笑)可能是参数。依赖代码依赖于数据,通过传递参数将数据注入。

    是的,这很愚蠢,而且它没有解决面向对象的依赖注入问题,但是函数式程序员会告诉你(如果你有一流的函数),这是你唯一需要的依赖注入。这里的重点是举一个简单的例子,展示潜在的问题。

    让我们来看看这个简单的传统函数——C++语法在这里并不重要,但我必须设法拼写它…

    void Say_Hello_World ()
    {
      std::cout << "Hello World" << std::endl;
    }
    

    我想提取并注入一个依赖项——文本“Hello World”。很简单。。。

    void Say_Something (const char *p_text)
    {
      std::cout << p_text << std::endl;
    }
    

    这怎么比原来的更不灵活?如果我决定输出应该是unicode呢。我可能想从std::cout切换到std::wcout。但这意味着我的字符串必须是wchar_t,而不是char。要么每个调用者都必须改变,要么(更合理地说),旧的实现被转换字符串并调用新实现的适配器所取代。

    这是维护工作,如果我们保留原版的话就不需要了。

    如果它看起来很简单,那么看看Win32 API中的这个实际函数。。。

    http://msdn.microsoft.com/en-us/library/ms632680%28v=vs.85%29.aspx

    这就是要处理的12个“依赖性”。例如,如果屏幕分辨率变得非常大,我们可能需要64位坐标值——以及另一个版本的CreateWindowEx。是的,已经有一个较旧的版本仍然存在,可能会在幕后映射到较新的版本。。。

    http://msdn.microsoft.com/en-us/library/ms632679%28v=vs.85%29.aspx

    这些“依赖项”不仅仅是原始开发人员的问题——使用该接口的每个人都必须查看依赖项是什么、它们是如何指定的,以及它们的含义,并确定如何为他们的应用程序做些什么。这就是“合理违约”一词可以让生活变得简单得多的地方。

    面向对象的依赖注入在原则上没有什么不同。无论是在源代码文本中还是在开发人员时间中,编写类都是一项开销,如果编写该类是为了根据某些依赖对象规范提供依赖项,那么依赖对象将被锁定为支持该接口,即使需要替换该对象的实现。

    所有这些都不应该被解读为声称依赖注入是不好的——远非如此。但是,任何好的技术都可能被过度地应用到错误的地方。正如不是每个字符串都需要提取出来并转化为参数一样,也不是每个低级行为都需要从高级对象中提取出来并转化为可注入的依赖项。

        3
  •  79
  •   Chris Laplante    13 年前

    以下是我自己的初步反应:基本上任何模式都有相同的缺点。

    • 学习需要时间
    • 如果被误解,可能弊大于利
    • 如果把它发挥到极致,它可能会比证明其好处的工作更多
        4
  •  46
  •   kyoryu    16 年前

    控制反转最大的“缺点”(不完全是DI,但足够接近)是,它倾向于去掉单一点来查看算法概述。不过,这基本上就是当你有解耦的代码时会发生的事情——在一个地方查看的能力是紧密耦合的产物。

        5
  •  44
  •   Andy Peck    15 年前

    在过去的6个月里,我一直在广泛使用Guice(JavaDI框架)。总的来说,我认为这很好(尤其是从测试的角度来看),但也有一些缺点。最值得注意的是:

    • 代码可能会变得更难理解。 依赖注入可以用于非常。。。创造性的方式。例如,我刚刚遇到一些使用自定义注释注入特定iostream的代码(例如:@Server1Stream、@Server2Stream)。虽然这确实有效,而且我承认它有一定的优雅性,但它使理解Guice注入成为理解代码的先决条件。
    • 学习项目时的学习曲线更高。 这与第一点有关。为了理解使用依赖注入的项目是如何工作的,您需要了解依赖注入模式和特定框架。当我开始目前的工作时,我花了好几个小时困惑地摸索着Guice在幕后做什么。
    • 构造函数变得庞大。 尽管这在很大程度上可以通过默认构造函数或工厂来解决。
    • 错误可能会被混淆。 我最近的一个例子是,我在两个旗名上发生了冲突。Guice默默地接受了错误,我的一个标志没有初始化。
    • 错误被推送到运行时。 如果配置不正确(循环引用、错误绑定等)大多数错误都不会在编译时被发现。相反,当程序实际运行时,错误就会暴露出来。

    现在我已经抱怨了。我要说的是,我将继续(自愿)在我当前的项目中使用Guice,而且很可能在我的下一个项目中使用Guice。依赖注入是一种非常好且功能强大的模式。但这肯定会让人困惑,你几乎肯定会花一些时间诅咒你选择的依赖注入框架。

    此外,我同意其他海报上的观点,即依赖注入可能会被过度使用。

        7
  •  27
  •   Anthony    15 年前

    没有任何DI的代码存在着众所周知的风险,可能会陷入 Spaghetti code -一些症状是类和方法太大,做的太多,不容易更改、分解、重构或测试。

    经常使用DI的代码可以 Ravioli code 每一个小班都像一块单独的馄饨块——它只做一件小事情,而 single responsibility principle 坚持,这是好的。但是单从类本身来看,很难看到系统作为一个整体做了什么,因为这取决于所有这些小部分是如何组合在一起的,这是很难看到的。它看起来就像一大堆小东西。

    通过避免一个大类中耦合代码的大位的意大利式复杂性,您将面临另一种复杂性的风险,其中有许多简单的小类,它们之间的交互是复杂的。

    我不认为这是一个致命的缺点——DI仍然非常值得。某种程度上的馄饨风格和只做一件事的小班教学可能是好的。即使过多,我也不认为它像意大利面代码那样糟糕。但意识到它可能会走得太远是避免它的第一步。按照链接讨论如何避免它。

        8
  •  13
  •   Community Mohan Dere    5 年前

    如果您有一个自主开发的解决方案,那么在构造函数中,依赖项就在您面前。或者作为方法参数,这也不难发现。尽管框架管理的依赖关系,如果走到极端,可能会开始像魔术一样出现。

    然而,在太多的类中有太多的依赖项是一个明显的迹象,表明你的类结构是错误的。因此,在某种程度上,依赖注入(自主开发或框架管理)有助于揭示隐藏在黑暗中的突出设计问题。


    为了更好地说明第二点,这里有一段摘录 article ( original source )我完全相信这是构建任何系统的根本问题,而不仅仅是计算机系统。

    假设你想设计一个大学校园。你必须把一些设计委托给学生和教授,否则物理大楼对物理专业的人来说就不好用了。没有一个架构师足够了解人们自己需要做什么。但你不能把每个房间的设计都委托给它的住户,因为那样你会得到一大堆瓦砾。

    如何在保持整体设计的一致性和协调性的同时,将设计责任分配到大型层次结构的所有级别?这是亚历山大试图解决的建筑设计问题,但也是计算机系统开发的一个基本问题。

    DI能解决这个问题吗? .但它确实能帮助你清楚地看到,你是否试图将设计每个房间的责任委托给住户。

        9
  •  13
  •   Alexander Derck    10 年前

    有一件事让我对DI有点不安,那就是假设所有注入的对象都是 例举起来很便宜 没有副作用 -或者——依赖关系的使用如此频繁,以至于它超过了任何相关的实例化成本。

    当依赖关系不存在时,这一点可能很重要 频繁地 在消费类中使用;比如说 IExceptionLogHandlerService 显然,这样的服务很少在类中被调用(希望是:)——可能只是在需要记录的异常上;然而 规范构造函数注入模式 ...

    Public Class MyClass
        Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService
    
        Public Sub New(exLogHandlerService As IExceptionLogHandlerService)
            Me.mExLogHandlerService = exLogHandlerService
        End Sub
    
         ...
    End Class
    

    ...要求提供这项服务的“实时”实例,并谴责实现这项服务所需的成本/副作用。不太可能,但如果构建这个依赖实例涉及服务/数据库命中、配置文件查找,或者锁定资源直到被释放,该怎么办?如果这项服务是根据需要、服务所在地或工厂生成的(都有自己的问题),那么您将只在必要时承担建设成本。

    现在,构建一个对象是一个普遍接受的软件设计原则 价廉物美 产生副作用。虽然这是个好主意,但情况并非总是如此。然而,使用典型的构造函数注入基本上要求这样做。这意味着,在创建依赖项的实现时,必须考虑DI。也许为了在其他地方获得好处,您会使对象构造的成本更高,但如果要注入这种实现,它可能会迫使您重新考虑该设计。

    顺便说一句,某些技术可以通过允许延迟加载注入的依赖项来缓解这个问题,例如提供一个类a Lazy<IService> 实例作为依赖项。这将改变依赖对象的构造器,使之更加了解实现细节,例如对象构造开销,这也可能是不可取的。

        10
  •  12
  •   Jack Leow    16 年前

    这更像是吹毛求疵。但依赖注入的一个缺点是,它使开发工具更难推理和导航代码。

    具体地说,如果在代码中控制方法调用的Click/Command Click,它将带您到接口上的方法声明,而不是具体的实现。

    这实际上是松散耦合代码(由接口设计的代码)的一个缺点,即使您不使用依赖注入(即,即使您只是使用工厂),它也适用。但是依赖注入的出现才是真正鼓励松耦合代码面向大众的原因,所以我想我应该提到它。

    此外,松散耦合代码的好处远远超过这一点,因此我称之为挑剔。尽管我已经工作了足够长的时间,知道如果你试图引入依赖注入,你可能会遇到这种阻力。

    事实上,我冒昧地猜测,对于依赖注入的每一个“缺点”,你都会发现许多远远超过它的优点。

        11
  •  12
  •   Joey Guerra    15 年前

    你只是通过实现依赖注入来实现代码的解耦,而没有实际解耦。我认为这是DI最危险的地方。

        12
  •  11
  •   sanityinc    15 年前

    基于构造函数的 依赖注入(不借助神奇的“框架”)是构造OO代码的一种干净而有益的方式。在我见过的最好的代码库中,在与Martin Fowler的其他前同事一起度过的几年里,我开始注意到,大多数以这种方式编写的好类最终都只有一个代码库 doSomething 方法

    那么,主要的缺点是,一旦你意识到这只是一种笨拙的、长期使用的面向对象的方式,将闭包作为类来编写,以获得函数式编程的好处,你编写面向对象代码的动机就会很快消失。

        13
  •  9
  •   James B    16 年前

    我发现构造函数注入可能会导致大而丑的构造函数(我在整个代码库中都使用它——也许我的对象太细粒度了?)。此外,有时构造函数注入会导致可怕的循环依赖(尽管这种情况非常罕见),因此您可能会发现自己必须在一个更复杂的系统中进行几轮依赖注入,从而获得某种就绪状态生命周期。

    然而,我更喜欢construtor注入而不是setter注入,因为一旦构建了我的对象,我就毫无疑问地知道它处于什么状态,无论它是在单元测试环境中还是加载在某个IOC容器中。以一种迂回的方式,这说明了我觉得塞特注射的主要缺点。

    (作为旁注,我确实觉得整个主题很“宗教化”,但你的里程数会随着开发团队的技术狂热程度而变化!)

        14
  •  8
  •   Mike Post    16 年前

    如果在没有IOC容器的情况下使用DI,最大的缺点是您很快就能看到代码实际上有多少依赖项,以及所有内容之间的紧密耦合程度。(“但我认为这是一个很好的设计!”)自然的进展是朝着一个IOC容器的方向发展,这个容器需要一点时间来学习和实现(虽然没有WPF学习曲线那么糟糕,但也不是免费的)。最后一个缺点是,一些开发人员将开始编写诚实的单元测试,这需要他们花时间来解决。以前可以在半天内搞定某件事的开发人员会突然花两天时间试图弄明白如何模仿他们所有的依赖关系。

    与Mark Seemann的答案类似,底线是你要花时间成为一名更好的开发人员,而不是把代码拼凑在一起,然后扔出大门/投入生产。你的公司更愿意选择哪一个?只有你能回答。

        15
  •  5
  •   Robert    16 年前

    DI是一种技术或模式,与任何框架都不相关。您可以手动连接依赖项。DI在SR(单一责任)和SoC(关注点分离)方面帮助您。DI带来了更好的设计。从我的观点和经验来看 没有坏处 .就像任何其他模式一样,你可能会弄错或误用它(但DI的情况非常困难)。

    如果将DI作为原则引入到遗留应用程序中,使用一个框架,那么您可能会犯的最大错误就是将其误用为服务定位器。DI+框架本身非常棒,它让我看到的每一个地方都变得更好!从组织的角度来看,每一种新的流程、技术、模式……都存在共同的问题:

    • 你必须训练你的团队
    • 您必须更改应用程序(包括风险)

    一般来说,你必须 投入时间和金钱 除此之外,真的没有坏处!

        16
  •  2
  •   Rahul    16 年前

    代码可读性。由于依赖项隐藏在XML文件中,因此您将无法轻松了解代码流。

        17
  •  2
  •   Martin Ellis    15 年前

    两件事:

    • 他们需要额外的工具支持来检查配置是否有效。

    例如,IntelliJ(商业版)支持检查Spring配置的有效性,并将标记错误,例如配置中的类型冲突。如果没有这种工具支持,您就无法在运行测试之前检查配置是否有效。

    这就是为什么“蛋糕”模式(Scala社区都知道)是个好主意的原因之一:组件之间的布线可以由类型检查器检查。注释或XML并没有这样的好处。

    • 这使得程序的全局静态分析变得非常困难。

    Spring或Guice等框架使得静态地确定容器创建的对象图将是什么样子变得困难。尽管它们在容器启动时创建对象图,但它们没有提供有用的API来描述要创建的对象图。

        18
  •  2
  •   Chris D    15 年前

    当你不断地使用技术来解决静态类型化问题时,静态类型化语言的好处似乎会显著减少。我刚刚采访过的一家大型Java商店正在用静态代码分析绘制它们的构建依赖关系。。。必须解析所有Spring文件才能有效。

        19
  •  1
  •   Ian Nelson    16 年前

    它可以增加应用程序启动时间,因为IoC容器应该以适当的方式解决依赖关系,有时需要进行多次迭代。

    推荐文章