为什么要使用依赖注入?


95

我很难找到有关为什么应该使用依赖项注入的资源。我看到的大多数资源都说明它只是将一个对象的实例传递给另一个对象的实例,但是为什么呢?这是仅用于更干净的体系结构/代码,还是会整体上影响性能?

为什么要执行以下操作?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

而不是以下?

class Profile {
    public function deactivateProfile()
    {
        $setting = new Setting();
        $setting->isActive = false;
    }
}

8
您正在将硬编码的依赖项引入deactivateProfile()(这很糟糕)。您在第一个代码中有更多的解耦代码,这使得更改和测试变得更加容易。
Aulis Ronkainen

3
你为什么要做第一个?您正在传递设置,然后忽略其值。
Phil N DeBlanc

57
我不同意那些赞成票。尽管可以认为该主题对专家而言是微不足道的,但这个问题还是有好处的:如果应该使用依赖倒置,那么应该有理由使用它。
扁平的

13
@PhilNDeBlanc:该代码显然过分简化,并不能真正表明现实世界的逻辑。但是,deactivateProfile对我来说,在isActive不关心其先前状态的情况下将设置为false是正确的方法。固有地调用该方法意味着您打算设置为非活动状态,而不是获取其当前(非活动)状态。
扁平的

2
您的代码不是依赖项注入或反转的示例。这是参数化的示例(通常比DI更好)。
jpmc26

Answers:


104

好处是无需依赖注入,您的Profile类

  • 需要知道如何创建设置对象(违反单一职责原则)
  • 始终以相同的方式创建其Settings对象(在两者之间创建紧密耦合)

但是有了依赖注入

  • 创建设置对象的逻辑在其他地方
  • 使用不同种类的设置对象很容易

在这种特定情况下,这似乎无关紧要,但是请想象一下,如果我们不是在谈论Settings对象,而是DataStore对象,它可能具有不同的实现,一个实现将数据存储在文件中,另一个实现将数据存储在文件中。数据库。对于自动化测试,您也需要模拟实现。现在,您确实不希望Profile类对使用的那个进行硬编码-更重要的是,您确实确实不希望Profile类了解文件系统路径,数据库连接和密码,因此创建DataStore对象必须发生在其他地方。


23
This may seem (or even be) irrelevant in this particular case实际上,我认为这非常相关。您将如何获得设置?我见过的许多系统将具有一组硬编码的默认设置和一个面向公众的配置,因此您需要加载这两者并用公共设置覆盖某些值。您甚至可能需要多个默认来源。也许您甚至可以从磁盘上获得一些,而从数据库中获得其他。因此,即使是获取设置的整个逻辑也常常是不平凡的-绝对不是耗费代码的事情应该或不会关心的事情。
VLAZ

我们还可以提到,对于一个非平凡的组件(例如Web服务)的对象初始化将使$setting = new Setting();效率极低。注入和对象实例化只发生一次。
vikingsteve

9
我认为使用模拟进行测试应该更加强调。如果您只看代码,它总是会成为一个Settings对象,并且永远不会改变,因此传入它似乎是一种浪费。但是,第一次尝试自行测试Profile对象而不需要Settings对象(使用模拟对象代替作为解决方案)时,这种需求非常明显。
JPhi1618

2
@ JPhi1618我认为强调“ DI用于单元测试”的问题在于,它仅会导致“为什么我需要单元测试”这一问题。答案对您来说似乎显而易见,而且好处肯定存在,但是对于刚开始的人来说,说“您需要做这件复杂的事情才能做这件复杂的事情”往往有点关掉。因此,最好提及各种优势,这些优势可能更适用于他们目前正在做的事情。
IMSoP

1
@ user986730-这是一个很好的观点,并强调了这里的真正原理是Dependency Inversion,其中Injection只是一种技术。因此,例如,在C语言中,您不能使用IOC库真正注入依赖项,但是您可以通过为您的模拟文件包含不同的源文件等,在编译时将它们反转,效果相同。
史蒂芬·伯恩

64

依赖注入使您的代码更易于测试。

当我负责修复Magento的PayPal集成中难以捕获的错误时,我是第一手学习的。

当贝宝告诉Magento付款失败时,就会出现问题:Magento无法正确记录失败。

“手动”测试潜在的修复程序将非常乏味:您需要以某种方式触发“失败的” PayPal通知。您必须提交电子支票,将其取消,然后等待其出错。这意味着需要3天以上才能测试一个字符的代码更改!

幸运的是,开发此功能的Magento核心开发人员似乎已经进行了测试,并使用依赖项注入模式使其变得微不足道。这使我们可以使用一个简单的测试用例来验证我们的工作:

<?php
// This is the dependency we will inject to facilitate our testing
class MockHttpClient extends Varien_Http_Adapter_Curl {
    function read() {
        // Make Magento think that PayPal said "VERIFIED", no matter what they actually said...
        return "HTTP/1.1 200 OK\n\nVERIFIED";
    }
}

// Here, we trick Magento into thinking PayPal actually sent something back.
// Magento will try to verify it against PayPal's API though, and since it's fake data, it'll always fail.
$ipnPayload = array (
  'invoice'        => '100058137',         // Order ID to test against
  'txn_id'         => '04S87540L2309371A', // Test PayPal transaction ID
  'payment_status' => 'Failed'             // New payment status that Magento should ingest
);

// This is what Magento's controller calls during a normal IPN request.
// Instead of letting Magento talk to PayPal, we "inject" our fake HTTP client, which always returns VERIFIED.
Mage::getModel('paypal/ipn')->processIpnRequest($ipnPayload, new MockHttpClient());

我确信DI模式还有很多其他优点,但是增加可测试性是我心中最大的收益

如果您对解决此问题感到好奇,请在此处查看GitHub存储库:https : //github.com/bubbleupdev/BUCorefix_Paypalstatus


4
依赖注入使代码比具有硬编码依赖的代码更易于测试。完全消除对业务逻辑的依赖甚至更好。
Ant P

1
执行@AntP建议的一种主要方法是通过参数化。将结果从数据库返回到用于填充页面模板的对象(通常称为“模型”)的逻辑甚至都不知道正在发生访存。它只需要获取那些对象作为输入即可。
jpmc26 '18

4
确实是@ jpmc26-我倾向于对功能性核心(命令式shell)进行细听,它实际上只是一个奇特的名字,用于将数据传递到您的域中,而不是在其中注入依赖项。域是纯净的,可以是单元没有嘲笑试验,然后是刚包裹在适应之类的持久性,消息等的壳
蚂蚁P

1
我认为仅关注可测试性对采用DI有害。对于那些觉得自己不需要太多测试或者认为自己已经控制了测试的人来说,它没有吸引力。我认为如果没有DI,编写干净,可重用的代码几乎是不可能的。测试在收益列表上非常遥远,令人失望的是,在关于DI收益的每个问题中,这个答案都排名第一或第二。
卡尔·莱斯

26

为什么(甚至是问题)?

为什么要使用依赖注入?

我为此找到的最好的助记符是“ new is gum ”:每次new在代码中使用时,该代码都将与该特定实现绑定在一起。如果重复使用new in构造函数,则将创建一系列特定的实现。而且由于您无法在没有构造类的情况下“拥有”该类的实例,因此无法分离该链。

例如,假设您正在编写赛车视频游戏。您从一个类开始,该类Game创建一个RaceTrack,创建一个8 Cars,每个创建一个Motor。现在,如果您想Cars以不同的加速度增加4个,则您必须更改提到的每个类,也许除外Game

清洁代码

这只是为了更清洁的架构/代码

是的

但是,在这种情况下,它似乎不太清楚,因为它是如何执行此操作的一个示例。实际的优势仅在涉及多个类时才显示出来,并且更难于演示,但是想象一下您将在前面的示例中使用DI。创建所有这些东西的代码可能看起来像这样:

List<Car> cars = new List<Car>();
for(int i=0; i<8; i++){
    float acceleration = 0.3f;
    float maxSpeed = 200.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
RaceTrack raceTrack = new RaceTrack(cars);
Game game = new Game(raceTrack);

现在可以通过添加以下行来添加这4种不同的汽车:

for(int i=0; i<4; i++){
    float acceleration = 0.5f;
    float maxSpeed = 100.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
  • 没有变化RaceTrackGameCar或者Motor是必要的-这意味着我们可以100%肯定,我们没有引入任何新的错误在那里!
  • 无需在多个文件之间跳转,您就可以同时在屏幕上看到完整的更改。这是将创建/设置/配置视为自己的责任的结果 -组装汽车不是汽车的工作。

性能考量

或这是否会影响整体绩效?

没有。但说实话,这可能会。

但是,即使在那种情况下,它也是如此的小,您无需关心。如果在将来的某个时候,您必须为tamagotchi编写具有5Mhz CPU和2MB RAM的代码,那么也许可能需要考虑这一点。

在99.999%*的情况下,它会具有更好的性能,因为您花费了更少的时间来修复错误,而花费了更多的时间来改进资源密集型算法。

* 完全由数字组成

添加的信息:“硬编码”

毫无疑问,这仍然是非常“硬编码” -数字是在代码中直接写入。不进行硬编码将意味着类似于将这些值存储在文本文件中(例如,以JSON格式),然后从该文件中读取它们。

为此,您必须添加代码以读取文件,然后解析JSON。如果您再次考虑该示例;在非DI版本中,a Car或a Motor现在必须读取文件。听起来好像没有太大意义。

在DI版本中,您可以将其添加到设置游戏的代码中。


3
广告采用硬编码,因此代码和配置文件之间并没有太大区别。与应用程序捆绑在一起的文件是源,即使您动态读取也是如此。除非将值由用户覆盖或取决于环境,否则将值从json或任何“ config”格式的数据中提取到数据文件无济于事。
Jan Hudec

3
我实际上曾经在Arduino(16MHz,2KB)上做过一次Tamagotchi
Jungkook

@JanHudec对。实际上,我在那里有一个较长的解释,但是为了使它更短而决定删除它,并专注于它与DI的关系。还有更多的东西不是100%正确的。总体而言,答案是最优化的,以推动DI的“要点”而不会太长。换句话说,这是我刚开始使用DI时想要听到的。
R. Schmitz

17

我总是对依赖注入感到困惑。它似乎只存在于Java领域中,但是这些领域对此表示了极大的敬意。您会看到,这是最棒的模式之一,据说可以使秩序混乱。但是这些示例总是令人费解并且是人为的,建立了一个非问题,然后着手通过使代码更复杂来解决它。

当同一个Python开发人员将这种智慧传授给我时,它变得更加有意义:它只是将参数传递给函数。这根本不是一个模式。更像是在提醒您,即使您自己可以提供合理的价值,您也可以要求提供一些参数作为论据。

因此,您的问题大致等同于“为什么我的函数应该接受参数?” 并具有许多相同的答案,即:让呼叫者做出决定

当然,这要付出一定的代价,因为现在您正在强迫调用者做出一些决定(除非您将参数设为可选),并且接口会更加复杂。作为交换,您将获得灵活性。

因此:您是否有充分理由特别需要使用此特定Setting类型/值?调用代码是否需要其他Setting类型/值是有充分理由的吗?(请记住,测试是代码!)


2
是的 当我意识到这仅仅是关于“让呼叫者做出决定”时,IoC终于向我点击。IoC意味着组件的控制权已从组件的作者转移到组件的用户。而且由于那时我已经对自己认为比我更聪明的软件有了足够的掌握,所以我立即以DI方法被出售。
Joker_vD

1
“它只是将参数传递给函数。根本就不是一种模式。”这是我所听到的关于DI的唯一好的甚至可理解的解释(很多人甚至不谈论它是什么,而是它的好处)。然后,他们使用复杂的行话,例如“控制反转”和“松散耦合”,当这是一个非常简单的想法时,只需要更多问题即可理解。
艾迪生

7

您提供的示例不是传统意义上的依赖项注入。依赖注入通常是指在构造函数中传递对象,或者是指在对象创建后立即使用“ setter注入”,以便在新创建的对象中的字段上设置值。

您的示例将对象作为参数传递给实例方法。然后,此实例方法修改该对象上的字段。依赖注入?否。破坏封装和数据隐藏?绝对!

现在,如果代码是这样的:

class Profile {
    private $settings;

    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    public function deactive() {
        $this->settings->isActive = false;
    }
}

然后我会说您正在使用依赖项注入。显着的区别是将一个Settings对象传递给构造函数或一个Profile对象。

如果Settings对象的构造昂贵或复杂,或者Settings是存在多个具体实现以更改运行时行为的接口或抽象类,则这很有用。

由于直接访问设置对象上的字段而不是调用方法,因此您无法利用多态性,这是依赖项注入的优点之一。

似乎配置文件的设置特定于该配置文件。在这种情况下,我将执行以下操作之一:

  1. 实例化Profile构造函数中的Settings对象

  2. 在构造函数中传递设置对象,然后复制应用于个人资料的各个字段

坦白地说,通过将Settings对象传入deactivateProfile,然后修改Settings对象的内部字段,便会产生代码味道。设置对象应该是唯一修改其内部字段的对象。


5
The example you give is not dependency injection in the classical sense.-没关系。面向对象的人在大脑上有物体,但是您仍然在依赖某种东西。
罗伯特·哈维

1
当您谈论“ 古典意义上的 ”时,就像@RobertHarvey所说的那样,您纯粹是用面向对象的方式说话。例如,在函数式编程中,将一个函数注入另一个函数(高阶函数)就是该范例依赖注入的经典示例。
David Arno

3
@RobertHarvey:我想我通过“依赖注入”获得了太多自由。人们最常想到该术语并使用该术语的地方是指在构造对象时或在进行setter注射后立即“注射”对象上的字段。
格雷格·伯格哈特

@DavidArno:是的,您是正确的。OP似乎在该问题中有一个面向对象的PHP代码段,因此我只是在这方面回答,而不是在解决功能性编程的问题-尽管使用PHP可以从功能性编程的角度提出相同的问题。
格雷格·伯格哈特

7

我知道我参加这个聚会要晚了,但是我感到很重要的一点是错过了。

我为什么要这样做:

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

你不应该 但这不是因为依赖注入是一个坏主意。这是因为这样做做错了。

让我们使用代码来看看这些东西。我们将这样做:

$profile = new Profile();
$profile->deactivateProfile($setting);

当我们从中得到相同的结果时:

$setting->isActive = false; // Deactivate profile

因此,当然这似乎是在浪费时间。就是这样的时候。这不是依赖注入的最佳用途。这甚至不是对类的最佳利用。

现在,如果相反,我们有这个:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

现在,application可以自由激活和停用,profile而不必特别了解setting实际更改的任何东西。为什么这么好?万一您需要更改设置。该application所以你可以自由地走在一个安全的空间,包含坚果,而无需只要你触摸的东西看打破一切从这些变化墙隔开。

这遵循行为原理的独立构造。DI模式在这里很简单。尽可能低地构建所需的所有内容,将它们连接在一起,然后通过一个电话启动所有行为。

结果是您有一个单独的地方来决定将什么连接到什么,而在另一个地方来管理什么对什么说了什么。

尝试对您必须长期维护的东西进行尝试,看看是否有帮助。


6

作为客户,当您雇用机械师为您的汽车做些什么时,您是否希望该机械师从头开始制造汽车然后再使用它?不,您给技工您想要他们工作的汽车

作为车库的所有者,当您指示机械师对汽车进行某些操作时,您是否希望机械师创建自己的螺丝刀/扳手/汽车零件?不,您向机械师提供了他需要使用的零件/工具

我们为什么要做这个?好吧,考虑一下。您是一位车库所有者,想雇用某人成为您的机械师。您将教他们成为机械师(=您将编写代码)。

将会变得更加容易:

  • 教机械师如何使用螺丝刀将扰流板安装到汽车上。
  • 教机械师创建汽车,创建扰流板,创建螺丝起子,然后使用新创建的螺丝起子将新创建的扰流板连接到新创建的汽车上。

不让您的机械师从头开始创建所有内容,这有很多好处:

  • 显然,如果您仅向技工提供现有的工具和零件,培训(=开发)就会大大缩短。
  • 如果同一位机械师必须多次执行相同的工作,则可以确保他重复使用螺丝刀,而不必总是扔掉旧的螺丝刀并创建新的螺丝刀。
  • 另外,学会了创造一切的机械师将需要更多的专家,因此期望更高的工资。这里的编码类比是,与具有单一严格定义的职责的类相比,具有许多职责的类的维护要困难得多。
  • 此外,当新发明进入市场时,扰流板现在是由碳而不是塑料制成的;您将必须重新培训(=重新开发)您的专家机械师。但是只要扰流板仍然可以以相同的方式连接,您的“简单”机械师就不必接受培训。
  • 拥有不依靠自己制造的汽车的机械师意味着您拥有能够处理他们可能收到的任何汽车的机械师。包括在培训机械师时还不存在的汽车。但是,您的专家机械师将无法制造训练制造的新车。

如果您雇用和培训专家机修工,最终将导致一名员工花费更多,花更多时间来完成本应做的简单工作,并且每当需要承担多种职责之一时,就永远需要对其进行培训要被更新。

开发类比是,如果您使用具有硬编码依赖性的类,那么最终将难以维护的类,无论何时创建新版本的对象(Settings就您而言),都需要不断的重新开发/更改,并且必须为类开发内部逻辑,以便能够创建不同类型的Settings对象。

此外,与使用类无关的任何人现在也将不得不要求类创建正确的Settings对象,而不是简单地能够将希望传递的任何Settings对象传递给类。这意味着用户需要进行其他开发,以弄清楚如何要求课程创建正确的工具。


是的,依赖反转需要花费更多的精力来编写,而不是对依赖进行硬编码。是的,必须输入更多内容很烦人。

但这与选择硬编码字面值的方法相同,因为“声明变量会花费更多的精力”。从技术上讲是正确的,但是专业人士的缺点要大几个数量级

创建应用程序的第一个版本时,没有经历依赖关系反转的好处。当您需要更改或扩展该初始版本时,可以体验依赖倒置的好处。并且不要欺骗自己以为您会在第一时间正确实现,并且不需要扩展/更改代码。您将不得不改变事情。


这会整体上影响性能吗?

这不会影响应用程序的运行时性能。但是,大量的影响了开发时间(因此性能)的的开发者


2
如果删除注释,“ 作为次要注释,您的问题集中在依赖倒置而不是依赖注入。注入是进行倒置的一种方法,但这不是唯一的方法。 ”,这将是一个很好的答案。依赖反转可以通过注入或定位器/全局变量来实现。该问题的示例与注入有关。因此,问题在于依赖注入(以及依赖倒置)。
David Arno

12
我认为整车的事情有点令人困惑和困惑
Ewan

4
@Flater,问题的一部分是没有人似乎真的同意依赖注入,依赖反转和控制反转之间的区别。可以肯定的是,最肯定不需要将依赖项注入到方法或构造函数中的“容器”。纯(或穷人)DI特别描述了手动依赖项注入。这是我个人使用的唯一依赖项注入,因为我不喜欢与容器关联的“魔术”。
David Arno

1
@Flater该示例的问题是,您几乎可以肯定不会以这种方式在代码中建模任何问题。因此,这是不自然的,强迫的,并且增加的问题多于答案。这使它更有可能误导和混淆而不是证明。
jpmc26

2
@gbjbaanb:我的回答根本没有深入研究多态性。没有继承,接口实现或类似类型上/下转换的任何操作。您完全误解了答案。
扁平的

0

像所有模式一样,询问“为什么”以避免过大的设计是非常有效的。

对于依赖项注入,通过考虑OOP设计的两个最重要的方面很容易看出这一点...

低耦合

计算机编程中的耦合

在软件工程中,耦合是软件模块之间的相互依赖程度;衡量两个例程或模块的紧密程度的度量;模块之间关系的强度。

您想要实现耦合。紧密关联的两件事意味着,如果您更改其中一项,则很可能必须更改另一项。一种错误或限制可能会导致另一种错误或限制;等等。

一类实例化另一个实例的对象是一种非常强的耦合,因为一个实例需要知道另一个实例的存在;它需要知道如何实例化它(构造函数需要哪些参数),并且这些参数在调用构造函数时必须可用。同样,根据语言是否需要显式解构(C ++),这将带来更多的复杂性。如果引入新的类(即a NextSettings或其他类),则必须返回到原始类,并向其构造函数添加更多调用。

高凝聚力

内聚力

在计算机编程中,内聚性是指模块内的元素所属的程度。

这是硬币的另一面。如果查看一个代码单元(一种方法,一个类,一个包等),则希望将所有代码包含在该单元内,以尽可能减少职责。

一个基本的例子就是MVC模式:您可以将域模型与视图(GUI)以及将它们粘合在一起的控制层清楚地分开。

这避免了代码膨胀,因为您会得到大块的代码,而这些大块代码会做很多不同的事情。如果您希望更改其中的某些部分,则还必须跟踪所有其他功能,并尝试避免错误等。然后您很快将自己编程到一个很难脱身的洞中。

使用依赖项注入,您可以将创建或跟踪依赖项委托给实现DI的任何类(或配置文件)。其他类将不太在乎到底发生了什么—它们将使用某些通用接口,并且不知道实际的实现是什么,这意味着它们对其他工作不承担任何责任


-1

当您遇到这些问题时,应该使用技巧来解决它们擅长解决的问题。依赖倒置和注入没有什么不同。

依赖倒置或注入是一种技术,它使您的代码可以决定在运行时调用哪种方法的实现。这使后期绑定的好处最大化。当语言不支持非实例函数的运行时替换时,该技术是必需的。例如,Java缺少将对静态方法的调用替换为对不同实现的调用的机制。与Python相反,替换函数调用所需的全部工作就是将名称绑定到另一个函数(重新分配包含该函数的变量)。

我们为什么要改变功能的实现?主要有两个原因:

  • 我们想使用伪造品进行测试。这使我们可以测试依赖于数据库提取的类,而无需实际连接到数据库。
  • 我们需要支持多种实现。例如,我们可能需要建立一个同时支持MySQL和PostgreSQL数据库的系统。

您可能还需要注意控制容器的反转。这项技术旨在帮助您避免看起来像此伪代码的大而纠结的构造树:

thing5 =  new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());

myApp = new MyApp(
    new MyAppDependency1(thing5, thing3),
    new MyAppDependency2(
        new Thing1(),
        new Thing2(new Thing3(thing5, new Thing4(thing5)))
    ),
    ...
    new MyAppDependency15(thing5)
);

它使您可以注册课程,然后为您完成构建:

injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);

myApp = injector.create(MyApp); // The injector fills in all the construction parameters.

请注意,如果注册的类可以是无状态单例,这是最简单的。

注意事项

需要注意的是依赖倒置应该不会是你去到回答去耦逻辑。寻找机会使用参数化。例如,考虑以下伪代码方法:

myAverageAboveMin()
{
    dbConn = new DbConnection("my connection string");
    dbQuery = dbConn.makeQuery();
    dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
    dbQuery.setParam("min", 5);
    dbQuery.Execute();
    myData = dbQuery.getAll();
    count = 0;
    total = 0;
    foreach (row in myData)
    {
        count++;
        total += row.x;
    }

    return total / count;
}

我们可以在此方法的某些部分中使用依赖项反转:

class MyQuerier
{
    private _dbConn;

    MyQueries(dbConn) { this._dbConn = dbConn; }

    fetchAboveMin(min)
    {
        dbQuery = this._dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    private _querier;

    Averager(querier) { this._querier = querier; }

    myAverageAboveMin(min)
    {
        myData = this._querier.fetchAboveMin(min);
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

但是我们不应该,至少不完全如此。请注意,我们已经使用创建了一个有状态Querier。现在,它具有对某些基本全局连接对象的引用。这会产生一些问题,例如难以理解程序的整体状态以及不同类之间如何相互配合。还要注意,如果我们要测试平均逻辑,则必须伪造查询器或连接。更好的方法是增加参数化

class MyQuerier
{
    fetchAboveMin(dbConn, min)
    {
        dbQuery = dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    averageData(myData)
    {
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

class StuffDoer
{
    private _querier;
    private _averager;

    StuffDoer(querier, averager)
    {
        this._querier = querier;
        this._averager = averager;
    }

    myAverageAboveMin(dbConn, min)
    {
        myData = this._querier.fetchAboveMin(dbConn, min);
        return this._averager.averageData(myData);
    }
}

连接将在更高的级别进行管理,该级别负责整个操作,并且知道如何处理此输出。

现在,我们可以完全独立于查询来测试平均逻辑,而且,我们可以在更广泛的情况下使用它。我们可能会质疑我们是否甚至需要MyQuerierand Averager对象,也许的答案是,如果我们不打算进行单元测试StuffDoer,那么我们就不需要了,而且StuffDoer由于单元测试与数据库紧密耦合,所以单元测试不是完全合理的。让集成测试覆盖它可能更有意义。在这种情况下,我们可能会精打细算fetchAboveMin并采用averageData静态方法。


2
依赖注入是一种旨在帮助您避免缠结的巨大构造树的技术…… ”。此声明之后的第一个示例是纯粹的或穷人的依赖项注入示例。第二个示例是使用IoC容器“自动”注入这些依赖项的示例。两者都是依赖注入实际上的例子。
David Arno

@DavidArno是的,您是对的。我已经调整了术语。
jpmc26

第三个主要原因是要更改实现,或者至少在假设实现可以更改的情况下设计代码:它激励开发人员松散耦合,并避免编写代码,如果某个新实现在某个时间实现,则很难更改。未来。尽管这在某些项目中可能不是优先考虑的事项(例如,知道应用程序在其初始发行后将永远不会被重新访问),但在其他项目中却是(例如,公司业务模型专门尝试为其应用程序提供扩展的支持/扩展) )。
扁平的

@Flater依赖注入仍然会导致强耦合。它将逻辑与特定接口联系起来,并要求相关代码知道该接口的作用。考虑用于转换从数据库获取的结果的代码。如果我使用DI来分隔它们,那么代码仍然需要知道发生DB提取并调用它。更好的解决方案是,如果转换代码甚至都不知道正在发生访存。您可以执行此操作的唯一方法是,如果调用者传递了获取的结果,而不是注入获取器。
jpmc26 '18

1
@ jpmc26但是在您的(注释)示例中,数据库甚至不需要成为依赖项即可。当然,避免依赖关系对于松散耦合会更好,但是DI专注于实现所需的依赖关系。
平坦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.