控制反转与依赖注入

709
根据Martin Fowler撰写的论文,控制反转是一种程序控制流逆转的原则: 程序员不再控制程序的流程,而是由外部来源(框架、服务、其他组件)接管控制。就像我们把某物插入另一个物品中。他举了EJB 2.0的例子:

例如Session Bean接口定义了ejbRemove,ejbPassivate(存储到辅助存储器)和ejbActivate(从被动状态恢复),您无法控制何时调用这些方法,只能控制它们执行的操作。容器会调用我们,我们不会调用它。

这导致了框架和库之间的差异:

控制反转是使框架与库不同的关键部分。库本质上是一组函数,您可以调用这些函数,现在通常组织成类。每个调用都会执行一些工作,然后返回控制权给客户端。

我认为,DI是IOC的观点意味着对象的依赖性被倒置:它不再控制自己的依赖关系、生命周期等,而是由其他东西为您完成。但是,正如您告诉我的那样,手动进行DI并不一定是IOC。我们仍然可以进行DI而没有IOC。

然而,在这篇文章中(来自另一个面向C/C++的IOC框架pococapsule),它建议由于IOC和DI,IOC容器和DI框架比J2EE更加优越,因为J2EE将框架代码混合到组件中,因此不能使其成为纯旧的Java/C++对象(POJO/POCO)。

除了依赖注入模式之外的控制反转容器(存档链接)

额外阅读以理解旧的基于组件的开发框架存在的问题,从而导致上述第二篇论文:控制反转的原因和内容(存档链接)

我的问题:什么是IOC和DI?我很困惑。根据pococapsule的说法,IOC不仅仅是对象或程序员与框架之间的控制反转。


5
这里有一篇关于IoC vs DI(依赖注入)vs SL(服务定位器)的好文章:http://tinyurl.com/kk4be58 - 从URL中提取:IoC vs DI(依赖注入)?IoC是一个通用概念,其中流程控制从客户端代码反转到框架,“为客户端执行某些操作”。SL(服务定位器)和DI(依赖注入)是从IoC衍生出来的两种设计模式。 - Swab.Jat
顺便说一句,如果有人对依赖注入在咖啡店主题中如何有帮助感兴趣,我在这里写了一篇文章:digigene.com/design-patterns/dependency-injection-coffeeshop - Ali Nem
3
初学者可读的不错文章:https://asimplify.com/dependency-injection-inversion-control/该文章介绍了依赖注入和控制反转的概念,使用通俗易懂的语言解释了它们在软件开发中的作用和意义。阅读本文可以让初学者更好地理解这两个重要的概念,并为之后的学习打下基础。 - Khawaja Asim
依赖倒置:依赖于抽象而不是具体实现。控制反转:主程序与抽象之间的关系,以及主程序是系统的粘合剂。以下是一些讨论此问题的好文章:https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/ https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/ https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/ - Daniel Andres Pelaez Lopez
1
阅读这篇深入的文章,它将澄清一切 https://martinfowler.com/articles/dipInTheWild.html#YouMeanDependencyInversionRight - Dusman
24个回答

817

控制反转(IoC)模式是提供任何类型的回调,代替我们直接执行操作来实现和/或控制反应的一种模式(换句话说,将控制反转和/或重定向到外部处理程序/控制器)。

例如,与其让应用程序调用由库(也称为工具包)提供的实现,库和/或框架会调用应用程序提供的实现。

依赖注入(DI)模式是IoC模式的更具体版本,其中实现通过构造函数/设置器/服务查找传递给对象,对象将依赖于这些实现以正确地运行。

每个DI实现都可以视为IoC,但不应该称之为IoC,因为实现依赖注入比回调更难(不要使用通用术语"IoC"降低产品价值)。

没有使用DI的IoC,例如,模板模式,因为实现只能通过子类化进行更改。

DI框架旨在利用DI,并可以定义接口(或Java中的注释)以轻松传递实现。

IoC容器是可以在编程语言之外工作的DI框架。在一些框架中,您可以在元数据文件(例如XML)中配置要使用的实现,这样就会更少干扰程序。有些框架可以做到通常不可能的IoC,比如在切入点处注入实现。

另请参阅Martin Fowler的文章


3
谢谢你的问题。但是另一篇文章表明,使用控制反转(IOC)容器要比使用企业Java Bean(EJB)更优越。而Martin Fowler则认为EJB是控制反转的典型例子。 - Amumu
8
EJB管理是IoC的典型例子,这可以从EJB的生命周期由容器而非程序员管理这一事实中看出。程序员不会创建或销毁EJB实例,因为控制权被委托给服务器。这就是IoC的概念:外部代码控制何时调用您的代码,通常与大多数情况下所做的相反。 - brandizzi
2
IoC 是一个通用术语,意思是框架调用应用程序提供的实现,而不是应用程序调用框架中的方法。您需要更详细地解释一下吗? - Imad Alazani
40
也称为“好莱坞原则”,意思是“不要打电话给我们,我们会给你打电话”。这种方法将调用权交给了框架而非应用程序。 - Garrett Hall
@ImadAlazani,你最好仔细阅读Garrett附上的文章,其中详细讨论了从应用程序代码到框架的控制反转。 - MengT
显示剩余2条评论

277

简而言之,IoC 是一个更广泛的术语,包括但不限于 DI。

“控制反转”(IoC)这个术语最初指的是一种编程风格,在这种风格中,整个框架或运行时控制程序流程。

在 DI 还没有名称的时候,人们开始将管理依赖关系的框架称为“控制反转容器”,随后,“控制反转”一词的含义逐渐漂移向特定的含义:对依赖项的控制反转。

控制反转(IoC)意味着对象不会创建它们依赖的其他对象来完成工作。相反,它们从外部源获取需要的对象(例如,一个 xml 配置文件)。

依赖注入(DI)意味着这是在对象不介入的情况下完成的,通常由框架组件传递构造函数参数和设置属性实现。


4
似乎IoC只是“依赖反转原则”的另一个术语,对吗? - Todd Vance
1
@ToddVance - 是的,我认为IoC和DIP是相同的东西。DIP和DI不是同一回事。IoC可以在没有DI的情况下完成,但是没有IoC就无法完成DI。 - Eljay
4
@ToddVance - 不,DIP和IoC不是同义词,也没有关联。 - TSmith
3
哈,这就是我在这个帖子中的原因...“控制反转 vs 依赖注入” - Todd Vance

102
依赖注入(DI)是一种实现控制反转(IoC)原则的设计模式,即将控制从对象转移到外部依赖项,从而将对象与其依赖项解耦并促进模块化。 Credit enter image description here source IoC(控制反转):这是一个通用术语,可以通过多种方式实现(事件、委托等)。
DI(依赖注入):DI是IoC的一个子类型,可以通过构造函数注入、setter注入或接口注入来实现。
但是,Spring仅支持以下两种类型:
  • Setter Injection
  • 基于setter的依赖注入是在调用用户的bean的无参构造函数或无参静态工厂方法实例化bean后,通过调用setter方法来实现的。
  • Constructor Injection
    • 基于构造函数的依赖注入是通过调用具有一定数量参数的构造函数来实现的,每个参数代表一个协作者。使用这种方式,我们可以验证注入的bean不为空,并且在编译时快速失败(在运行时而不是运行时失败),因此在启动应用程序时我们会得到NullPointerException: bean does not exist。构造函数注入是注入依赖项的最佳实践。

3
错误的说法是Spring不支持属性注入,实际上它是支持的。然而,我同意这是一种不好的实践方法。 - kekko12
1
Spring的@Autowired注解是一种属性注入的方式,我认为。 - Sajith
2
我认为IoC很可能是将对象依赖委托给更高层级的原则,而DI是应用IoC的一种方式。 - Ahmed Saied

58

DI是IoC的一个子集。

  • IoC 的意思是对象不创建它们所依赖的其他对象来完成工作。相反,它们从外部服务(例如xml文件或单个应用程序服务)获取它们需要的对象。我使用的IoC的两种实现方式是 DI 和 ServiceLocator。
  • DI 意味着通过使用抽象(接口)而不是具体对象来完成依赖对象的IoC原则。这使得所有组件都能进行链式测试,因为高层次组件只依赖于接口,而不是低层次组件。模拟实现这些接口。

这里有一些实现IoC的其他技术。


1
我不会说IoC意味着不创建对象。当您不直接调用类方法,而是接口方法时 - 这就是控制反转(在这种情况下,调用者不依赖于调用代码),它与对象创建无关。 IoC的另一个例子是事件和委托。 - Evgeny Gorbovoy
不。IoC适用于除了对象创建之外的许多情境。而DI与你是否使用接口或具体类来定义合同无关。 - undefined

43

IOC (控制反转):将控制权交给容器以获取对象实例称为控制反转,这意味着不再使用new运算符来创建对象,而是通过容器来实现。

DI (依赖注入):将属性注入到对象中的方式被称为依赖注入。

我们有三种类型的依赖注入:

  1. 构造函数注入
  2. Setter/Getter注入
  3. 接口注入

Spring仅支持构造函数和Setter/Getter注入。


1
IoC不需要容器 - 这只是一种使其更方便的便捷方式。 - Nate Gardner

39

由于所有的答案都强调理论,因此我希望首先通过示例演示:

假设我们正在构建一个应用程序,其中包含一项功能,即在订单发货后发送短信确认消息。 我们将有两个类,一个负责发送短信(SMSService),另一个负责捕获用户输入(UIHandler),我们的代码如下:

public class SMSService
{
    public void SendSMS(string mobileNumber, string body)
    {
        SendSMSUsingGateway(mobileNumber, body);
    }

    private void SendSMSUsingGateway(string mobileNumber, string body)
    {
        /*implementation for sending SMS using gateway*/
    }
}

public class UIHandler
{
    public void SendConfirmationMsg(string mobileNumber)
    {
        SMSService _SMSService = new SMSService();
        _SMSService.SendSMS(mobileNumber, "Your order has been shipped successfully!");
    }
}
上述实现并没有错误,但存在一些问题: -) 假设在开发环境下,你想将发送的短信保存到文本文件而不是使用SMS网关,为了实现这一点,我们将不得不更改(SMSService)的具体实现,这样我们失去了灵活性并被迫在这种情况下重写代码。 -) 我们最终会混合类的职责,我们的(UIHandler)不应该知道(SMSService)的具体实现,这应该在使用“接口”的类之外完成。当这个被实现后,我们将有能力通过替换所使用的(SMSService)与另一个实现相同接口的模拟服务来改变系统行为,该服务将把短信保存到文本文件中而不是发送到手机号码。
为了解决上述问题,我们使用接口,它将由我们的(SMSService)和新的(MockSMSService)来实现,基本上新接口(ISMSService)将公开两个服务的相同行为,如下面的代码所示:
public interface ISMSService
{
    void SendSMS(string phoneNumber, string body);
}

那么我们将更改(SMSService)的实现以实现(ISMSService)接口:

public class SMSService : ISMSService
{
    public void SendSMS(string mobileNumber, string body)
    {
        SendSMSUsingGateway(mobileNumber, body);
    }

    private void SendSMSUsingGateway(string mobileNumber, string body)
    {
        /*implementation for sending SMS using gateway*/
        Console.WriteLine("Sending SMS using gateway to mobile: 
        {0}. SMS body: {1}", mobileNumber, body);
    }
}

现在我们可以使用相同的接口创建全新的模拟服务(MockSMSService),并采用完全不同的实现方式:

public class MockSMSService :ISMSService
{
    public void SendSMS(string phoneNumber, string body)
    {
        SaveSMSToFile(phoneNumber,body);
    }

    private void SaveSMSToFile(string mobileNumber, string body)
    {
        /*implementation for saving SMS to a file*/
        Console.WriteLine("Mocking SMS using file to mobile: 
        {0}. SMS body: {1}", mobileNumber, body);
    }
}

此时,我们可以在(UIHandler)中轻松地更改代码以使用服务的具体实现(MockSMSService),方法如下:

public class UIHandler
{
    public void SendConfirmationMsg(string mobileNumber)
    {
        ISMSService _SMSService = new MockSMSService();
        _SMSService.SendSMS(mobileNumber, "Your order has been shipped successfully!");
    }
}

我们在代码中实现了很多灵活性并实现了关注点分离,但是我们仍然需要对代码进行更改,以在两个短信服务之间切换。因此我们需要实现依赖注入

为了实现这一点,我们需要对(UIHandler)类构造函数进行更改,通过它传递依赖项。通过这样做,使用(UIHandler)的代码可以确定要使用哪种(ISMSService)的具体实现:

public class UIHandler
{
    private readonly ISMSService _SMSService;

    public UIHandler(ISMSService SMSService)
    {
        _SMSService = SMSService;
    }

    public void SendConfirmationMsg(string mobileNumber)
    {
        _SMSService.SendSMS(mobileNumber, "Your order has been shipped successfully!");
    }
}

现在负责与类(UIHandler)通信的UI表单将要传递哪个接口实现(ISMSService)来消耗。这意味着我们已经颠倒了控制,(UIHandler)不再负责决定使用哪个实现,调用代码会决定。我们已经实现了控制反转原则,其中DI是其中的一种类型。

UI表单代码如下:

class Program
{
    static void Main(string[] args)
    {
        ISMSService _SMSService = new MockSMSService(); // dependency

        UIHandler _UIHandler = new UIHandler(_SMSService);
        _UIHandler.SendConfirmationMsg("96279544480");

        Console.ReadLine();
    }
}

如果您也提供了一个没有依赖注入的IOC示例,那就太好了。例如,基于XML的IOC系统。 - Ozkan

38
与其直接对比DI和IoC,从头开始可能更有帮助:每个非平凡的应用程序都依赖于其他代码片段。
所以我正在编写一个类MyClass,我需要调用YourService的方法...不知道如何获取YourService的实例。最简单、最直接的方法是自己实例化它。 YourService service = new YourServiceImpl(); 直接实例化是获取依赖项的传统(过程式)方式。但它有许多缺点,包括将MyClass紧密耦合到YourServiceImpl,使我的代码难以更改和测试。MyClass不关心YourService的实现方式,因此MyClass不想负责实例化它。
我更愿意将这种责任从MyClass转移到MyClass之外的某些东西。最简单的方法就是将实例化调用(new YourServiceImpl();)移动到其他类中。我可能会将这个其他类命名为定位器、工厂或任何其他名称;但重点是MyClass不再负责YourServiceImpl。我已经倒置了这种依赖。很好。
问题是,MyClass仍然负责调用定位器/工厂/任何其他东西。由于我只是插入了一个中间人来倒置依赖关系,现在我与中间人耦合在一起(即使我没有与中间人提供的具体对象耦合)。
我真的不关心我的依赖项来自哪里,所以我宁愿不负责检索它们。仅仅倒置依赖关系还不够。我想倒置整个过程的控制权。
我需要一个完全独立的代码片段,MyClass将其插入(称之为框架)。然后,我唯一剩下的责任就是声明对YourService的依赖关系。框架可以负责确定何时、何地和如何获取实例,并给出MyClass所需的内容。最好的部分是MyClass不需要知道框架的存在。框架可以控制这个依赖关系连接过程。现在我已经倒置了控制(在倒置依赖关系之上)。
MyClass连接到框架的方法有不同的方式。注入是一种机制,我只需声明一个字段或参数,我希望框架在实例化MyClass时提供它。

我认为所有这些概念之间的关系层次比本主题中其他图表所显示的略微复杂,但基本思想是它是一种分层关系。我认为这与实际应用中的依赖反转原则相吻合。

Hierarchy of Dependency Acquisition


3
我喜欢这个答案涉及到工厂如何适应所有这些,并提供了一个漂亮的概述图,展示了不同的概念以及它们之间的关系。 - Haroon

8

2
我猜他们试图解决的问题是,DI是IoC设计模式的一种非常广泛使用的风格,以至于它几乎可以轻松地被称为IoC aka DI - 除非文档有任何明确的参考建议。 - ha9u63a7
6
IoC也被称为依赖注入(DI)。 - Michael M
2
IoC可以被称为设计原则,而DI是其实现。在这两种情况下,管理对象的责任被转移给了Spring容器,因此实现了控制反转。 - Tariq Abbas
由于这个声明,面试者为了捍卫这个声明而变得疯狂,因为他没有其他信息来源。相当具有误导性... - Praytic
更新的链接是:https://docs.spring.io/spring-framework/reference/core/beans/introduction.html - undefined

8

IoC - 控制反转是一个通用的术语,与语言无关,它实际上不创建对象,而是描述对象以何种方式被创建。

DI - 依赖注入是具体术语,在其中我们通过使用不同的注入技术(如Setter Injection、Constructor Injection或Interface Injection)在运行时提供对象的依赖项。


7
控制反转是一种设计范例,旨在将更多的控制权交给应用程序中的目标组件,即那些完成工作的组件。
依赖注入是一种模式,用于创建对象实例,这些对象依赖其他对象,而不知道在编译时将使用哪个类来提供该功能。
有几种基本的技术可以实现控制反转。这些包括:
- 使用工厂模式 - 使用服务定位器模式 - 使用以下任何一种类型的依赖注入:
1. 构造函数注入 2. 设置器注入 3. 接口注入

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接