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

为什么要执行以下操作?

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


以下是什么?

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


评论

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

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

我不同意那些赞成票。尽管该主题对于专家而言可能是微不足道的,但这个问题还是值得的:如果应该使用依赖倒置,那么应该有理由使用它。

@PhilNDeBlanc:该代码显然过分简化,并不能真正表明现实世界的逻辑。但是,deactivateProfile向我建议,将isActive设置为false而不关心其先前状态是此处的正确方法。固有地调用该方法意味着您要将其设置为非活动状态,而不是获取其当前(非活动)状态。

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

#1 楼

优点在于,没有依赖项注入,您的Profile类


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

但是通过依赖项注入


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

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

评论


实际上,在这个特定情况下,这似乎无关紧要,甚至是无关紧要的。您将如何获得设置?我见过的许多系统将具有一组硬编码的默认设置和一个面向公众的配置,因此您需要加载这两者并用公共设置覆盖某些值。您甚至可能需要多个默认来源。也许您甚至可以从磁盘上获得某些东西,而从数据库中获得其他东西。因此,即使是获取设置的整个逻辑也常常是不平凡的-绝对不是耗费代码的事情应该或不会在乎的。

– VLAZ
18年11月13日在14:55

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

–vikingsteve
18年11月14日在8:55

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

–JPhi1618
18年11月14日在16:49

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

–IMSoP
18-11-14在18:09



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

–斯蒂芬·伯恩
18-11-15在11:28

#2 楼

依赖注入使您的代码更易于测试。
当我负责修复Magento的PayPal集成中难以捕获的错误时,我是第一手学习的。
当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

评论


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

– Ant P
18年11月13日在15:28

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

– jpmc26
18年11月13日在22:22



确实是@ jpmc26-我倾向于对功能核心(命令式外壳)进行细听,它实际上只是用于将数据传递到您的域中而不是将依赖项注入其中的奇特名称。该域是纯域,可以在不进行任何模拟的情况下进行单元测试,然后将其包装在可适应持久性,消息传递等功能的外壳中。

– Ant P
18-11-14在8:27



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

–卡尔·莱斯(Carl Leth)
18年11月15日在7:57

#3 楼

为什么(甚至是什么问题)?


为什么我应该使用依赖项注入?


我为此找到的最好的助记词是“ new is胶水”:每次您在代码中使用new时,该代码都会与该特定实现相关联。如果重复使用new in构造函数,则将创建一系列特定的实现。而且,因为如果不构造类就不能“拥有”类的实例,就无法分离该链。

例如,假设您正在编写赛车视频游戏。您从类Game开始,该类创建一个RaceTrack,该类创建8个Cars,每个都创建一个Motor。现在,如果您想要另外4个具有不同加速度的Cars,则必须更改每个提及的类,也许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);
}



无需对RaceTrackGameCarMotor进行更改-这意味着我们可以100%确保没有在此处引入任何新的错误!
不必在多个文件之间跳转,您就可以同时在屏幕上看到完整的更改。这是因为将创建/设置/配置视为自己的责任,这不是汽车制造马达的工作。

性能注意事项


还是会整体上影响性能?


不会。但是说实话,这可能会。

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

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

*完全由数字组成

添加的信息:“硬编码”

没错,这仍然是很多“硬编码” “-数字直接写在代码中。没有硬编码将意味着类似于将这些值存储在文本文件中-例如以JSON格式-然后从该文件中读取它们。

为此,您必须添加用于读取文件然后解析JSON的代码。如果您再次考虑该示例;在非DI版本中,CarMotor现在必须读取文件。听起来似乎没有太大意义。

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

评论


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

– Jan Hudec
18-11-14在8:11



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

– Jungkook
18年11月14日在9:54

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

–R。Schmitz
18年11月14日在12:19

#4 楼

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

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

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

当然,这是有代价的,因为现在您正在强迫呼叫者做出一些决定(除非您做出决定参数是可选的),则界面会更加复杂。作为交换,您会获得灵活性。

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

评论


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

– Joker_vD
18-11-14在23:06

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

–艾迪生
19年5月13日在18:34



#5 楼

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

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

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

 class Profile {
    private $settings;

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

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


那我要说你正在使用依赖注入。显着的不同是将Settings对象传递给构造函数或Profile对象。

如果Settings对象昂贵或构造复杂,或者Settings是接口或抽象类,其中多个为了更改运行时行为,存在一些具体的实现。

由于您直接访问Settings对象上的字段而不是调用方法,因此您无法利用Polymorphism,它是其中的一种依赖注入的好处。

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


实例化Profile构造函数中的Settings对象
在构造函数中传递Settings对象,然后复制适用于简而言之,通过将Settings对象传递到deactivateProfile,然后修改Settings对象的内部字段,这是一种代码味道。 Settings对象应该是修改其内部字段的唯一对象。

评论


您提供的示例不是传统意义上的依赖项注入。 -没关系。面向对象的人在大脑上有物体,但是您仍然在依赖某种东西。

–罗伯特·哈维(Robert Harvey)
18年11月13日在16:47

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

– David Arno
18年11月13日在17:11

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

– Greg Burghardt
18年11月13日在19:26

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

– Greg Burghardt
18年11月13日在19:45

#6 楼

我知道我要晚参加这个聚会,但是我觉得错过了一个重要的观点。


我为什么要这样做:


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模式在这里很简单。尽可能低地构建所需的所有内容,将它们连接在一起,然后通过一个电话开始所有行为的滴答作响。

结果是您有一个单独的地方来决定什么连接到什么,以及

尝试在必须随时间推移维护的东西上尝试一下,看看是否有帮助。

#7 楼

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

作为车库的主人,当您指示技工对汽车进行某些操作时,您希望技工自己制造螺丝刀吗? /扳手/汽车零件?不,您为机械师提供了他需要使用的零件/工具

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

会变得更容易:


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

不让技工创建具有很多好处从头开始的一切:


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

如果您雇用和培训专家机械师,最终将导致一名雇员付出代价更多,需要花费更多时间来执行本应简单的工作,并且每当需要更新其许多职责之一时,就将永远需要接受培训。

开发类比是,如果您使用类具有硬编码的依赖关系,那么最终将难以维护的类,每当创建新版本的对象(在您的情况下为Settings)时,都需要不断的重新开发/更改,并且您必须为该对象开发内部逻辑该类具有创建不同类型的Settings对象的能力。

此外,无论谁消耗了您的类,现在还必须要求该类创建正确的Settings对象,而不是简单地能够将希望传递的任何Settings对象传递给该类。这意味着需要用户进行其他开发,以弄清楚如何要求类创建正确的工具。


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

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

创建应用程序的第一个版本时,没有体验到依赖倒置的好处。当您需要更改或扩展该初始版本时,可以体验依赖倒置的好处。并且不要欺骗自己以为您会在第一时间正确使用它,并且不需要扩展/更改代码。您将不得不进行更改。



这是否会整体上影响性能?


这不会影响运行时的性能。应用程序。但这会严重影响开发人员的开发时间(进而影响性能)。

评论


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

– David Arno
18-11-13在12:21

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

–伊万
18年11月13日在13:16

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

– David Arno
18年11月13日在13:29

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

– jpmc26
18年11月13日在16:50



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

–更
18-11-15在12:17



#8 楼

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

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

低耦合度

计算机编程中的耦合度:


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


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

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

高内聚力

内聚力:


在计算机编程中,内聚性是指模块内元素相互之间结合的程度。


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

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

这样可以避免代码膨胀,因为您会得到大块的代码,这些大块代码可以执行许多不同的操作。如果您希望更改其中的某些部分,则还必须跟踪所有其他功能,并尝试避免错误等。然后您很快就会陷入难以摆脱的困境。

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

#9 楼

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

依赖关系反转或注入是一种技术,它允许您的代码确定在运行时调用方法的哪种实现。这使后期绑定的好处最大化。当语言不支持非实例函数的运行时替换时,该技术是必需的。例如,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);
    }
}


连接将在更高的级别进行管理,该级别负责整个操作,并且知道该怎么做。现在,我们可以完全独立于查询来测试平均逻辑,此外,我们可以在更广泛的情况下使用它。我们可能会质疑我们是否甚至需要MyQuerierAverager对象,也许答案是我们不打算对单元格StuffDoer进行单元测试,而不是对单元格StuffDoer进行单元测试是完全合理的,因为它与数据库。让集成测试覆盖它可能更有意义。在这种情况下,我们可以将fetchAboveMinaverageData设为静态方法。

评论


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

– David Arno
18年11月13日在16:13

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

– jpmc26
18年11月13日在16:47

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

–更
18年11月15日在7:06

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

– jpmc26
18-11-15在16:47



@ jpmc26但在您的(注释)示例中,数据库甚至不需要成为依赖项即可。当然,避免依赖关系对于松散耦合会更好,但是DI专注于实现所需的依赖关系。

–更
18-11-15在16:53