IOC(控制反转)和DI(依赖注入)

本文深入浅出地介绍了IoC(控制反转)的概念及其在Laravel框架中的应用,通过实例演示了依赖注入的基本原理,探讨了IoC模式的优势与潜在问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、名词介绍

laravel官网介绍

Laravel使用IoC(Inversion of Control,控制倒转,这是一个设计模式,可以先查看下百科)容器这个强有力的工具管理类依赖。依赖注入(也是一种设计模式,一般用于实现IoC)是一个不用编写固定代码来处理类之间依赖的方法,相反的,这些依赖是在运行时注入的,这样允许处理依赖时具有更大的灵活性。


简单粗暴的理解:

依赖注入和控制反转说的是同一个东西,是一种设计模式,这种设计模式用来减少程序间的耦合。


二、IOC的理论背景

我们知道在面向对象设计的软件系统中,它的底层都是由N个对象构成的,各个对象之间通过相互合作,最终实现系统地业务逻辑[1]。

  图1 软件系统中耦合的对象

如果我们打开机械式手表的后盖,就会看到与上面类似的情形,各个齿轮分别带动时针、分针和秒针顺时针旋转,从而在表盘上产生正确的时间。图1中描述的就是这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项任务。我们可以看到,在这样的齿轮组中,如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转。

  齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。

  图2 对象之间的依赖关系

耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家Michael Mattson 1996年提出了IOC理论,用来实现对象之间的“解耦”,目前这个理论已经被成功地应用到实践当中。

 

IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。如下图:

 

  图3 IOC解耦过程

  大家看到了吧,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。

 

  我们再来做个试验:把上图中间的IOC容器拿掉,然后再来看看这套系统:

 

图4 拿掉IOC容器后的系统

  

  我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,A、B、C、D这4个对象之间已经没有了耦合关系,彼此毫无联系,这样的话,当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现IOC容器,对于系统开发而言,这将是一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系!

 

    我们再来看看,控制反转(IOC)到底为什么要起这么个名字?我们来对比一下: 

    软件系统在没有引入IOC容器之前,如图1所示,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。 

    软件系统在引入IOC容器之后,这种情形就完全改变了,如图3所示,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。 

    通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。


三、示例演示

1、先假设我们这里有一个类,类里面需要用到数据库连接,按照最最原始的办法,我们可能是这样写这个类的:

<?php
class  example {
     private  $_db ;
 
     function  __construct(){
         include  "./Lib/Db.php" ;
         $this ->_db =  new  Db( "localhost" , "root" , "123456" , "test" );
     }
 
     function  getList(){
         //这里具体sql语句就省略不写了
         $this ->_db->query( "......" );
     }
}

过程:

在构造函数里先将数据库类文件include进来;
然后又通过new Db并传入数据库连接信息实例化db类;
之后getList方法就可以通过$this->_db来调用数据库类,实现数据库操作。

 

看上去我们实现了想要的功能,但是这是一个噩梦的开始,以后example1,example2,example3....越来越多的类需要用到db组件,如果都这么写的话,万一有一天数据库密码改了或者db类发生变化了,岂不是要回头修改所有类文件?
ok,为了解决这个问题,工厂模式出现了,我们创建了一个Factory方法,并通过Factory::getDb()方法来获得db组件的实例:

<?php
class  Factory {
     public  static  function  getDb(){
         include  "./Lib/Db.php" ;
         return  new  Db( "localhost" , "root" , "123456" , "test" );
     }
}


2、sample类变成: 

<?php
class  example {
 
     private  $_db ;
     function  __construct(){
         $this ->_db = Factory::getDb();
     }
     
     function  getList(){
         $this ->_db->query( "......" ); //这里具体sql语句就省略不写了
     }
}

这样就完美了吗?再次想想一下以后example1,example2,example3....所有的类,你都需要在构造函数里通过Factory::getDb();获的一个Db实例,实际上你由原来的直接与Db类的耦合变为了和Factory工厂类的耦合,工厂类只是帮你把数据库连接信息给包装起来了,虽然当数据库信息发生变化时只要修改Factory::getDb()方法就可以了,但是突然有一天工厂方法需要改名,或者getDb方法需要改名,你又怎么办?当然这种需求其实还是很操蛋的,但有时候确实存在这种情况,一种解决方式是:

 

3、我们不从example类内部实例化Db组件,我们依靠从外部的注入,什么意思呢?看下面的例子:

<?php
class  example {
     private  $_db ;
     function  getList(){
         $this ->_db->query( "......" ); //这里具体sql语句就省略不写了
     }
     //从外部注入db连接
     function  setDb( $connection ){
         $this ->_db =  $connection ;
     }
}
//调用
$example  new  example();
$example ->setDb(Factory::getDb()); //注入db连接
$example ->getList();

 

这样一来,example类完全与外部类解除耦合了,你可以看到Db类里面已经没有工厂方法或Db类的身影了。我们通过从外部调用example类的setDb方法,将连接实例直接注入进去。这样example完全不用关心db连接怎么生成的了。
这就叫依赖注入,实现不是在代码内部创建依赖关系,而是让其作为一个参数传递,这使得我们的程序更容易维护,降低程序代码的耦合度,实现一种松耦合。

 

这还没完,我们再假设example类里面除了db还要用到其他外部类,我们通过:

<?php
$example ->setDb(Factory::getDb()); //注入db连接
$example ->setFile(Factory::getFile()); //注入文件处理类
$example ->setImage(Factory::getImage()); //注入Image处理类

我们没完没了的写这么多set?累不累?

 

4、ok,为了不用每次写这么多行代码,我们又去弄了一个工厂方法: 

<?php
class  Factory {
     public  static  function  getExample(){
         $example  new  example();
         $example ->setDb(Factory::getDb()); //注入db连接
         $example ->setFile(Factory::getFile()); //注入文件处理类
         $example ->setImage(Factory::getImage()); //注入Image处理类
         return  $expample ;
     }
}

实例化example时变为: 

<?php
$example =Factory::getExample();
$example ->getList();

似乎完美了,但是怎么感觉又回到了上面第一次用工厂方法时的场景?这确实不是一个好的解决方案,所以又提出了一个概念:容器,又叫做IoC容器、DI容器。

我们本来是通过setXXX方法注入各种类,代码很长,方法很多,虽然可以通过一个工厂方法包装,但是还不是那么爽,好吧,我们不用setXXX方法了,这样也就不用工厂方法二次包装了,那么我们还怎么实现依赖注入呢?

 

5、这里我们引入一个约定:在example类的构造函数里传入一个名为Di $di的参数,如下: 

<?php
class  example {
     private  $_di ;
     function  __construct(Di & $di ){
         $this ->_di =  $di ;
     }
     //通过di容器获取db实例
     function  getList(){
         $this ->_di->get( 'db' )->query( "......" ); //这里具体sql语句就省略不写了
     }
}
 
$di  new  Di();
$di ->set( "db" , function (){
     return  new  Db( "localhost" , "root" , "root" , "test" );
});
  
//在容器中注册一个filter服务
$di ->set( 'filter' function () {
     return  new  Filter();
});
 
//在容器中注册一个session服务
$di ->set( 'session' function () {
     return  new  Session();
});
$example  new  example( $di );
$example ->getList();

Di就是IoC容器,所谓容器就是存放我们可能会用到的各种类的实例,我们通过$di->set()设置一个名为db的实例,因为是通过回调函数的方式传入的,所以set的时候并不会立即实例化db类,而是当$di->get('db')的时候才会实例化,同样,在设计di类的时候还可以融入单例模式。

这样我们只要在全局范围内申明一个Di类,将所有需要注入的类放到容器里,然后将容器作为构造函数的参数传入到example,即可在example类里面从容器中获取实例。当然也不一定是构造函数,你也可以用一个 setDi(Di $di)的方法来传入Di容器,总之约定是你制定的,你自己清楚就行。


四、laravel中的实际用法

以上我们了解IOC这种设计模式,接下来我们来看看laravel里面的$app的各个类是如何注入进去的。

资料参考:http://www.golaravel.com/laravel/docs/4.2/lifecycle/

翻代码找到入口文件:

1.文件路径:\public\index.php

$app  require_once  __DIR__. '/../bootstrap/start.php' ;

2.文件路径:\bootstrap\start.php

$app  new  Illuminate\Foundation\Application;
require  $framework . '/Illuminate/Foundation/start.php' //line 58

3.vendor start,文件路径:\paf-vendor\laravel\v4.2.11\vendor\laravel\framework\src\Illuminate\Foundation\start.php

$providers  $config [ 'providers' ];
$app ->getProviderRepository()->load( $app $providers );

4.文件

\v4.2.11\vendor\laravel\framework\src\Illuminate\Foundation\ProviderRepository.php

\v4.2.11\vendor\laravel\framework\src\Illuminate\Foundation\Application.php

1.ProviderRepository->load();
2.ProviderRepository->compileManifest();
     $app ->register( $this ->createProvider( $app $provider ));
3.Application->register();
     $provider ->register();

回过头来看看我们的每个provider怎么写的:\v4.2.11\vendor\laravel\framework\src\Illuminate\Cookie\CookieServiceProvider.php

/**
  * Register the service provider.
  *
  * @return void
  */
public  function  register()
{
    $this ->app->bindShared( 'cookie' function ( $app )
    {
       $config  $app [ 'config' ][ 'session' ];
 
       return  ( new  CookieJar)->setDefaultPathAndDomain( $config [ 'path' ],  $config [ 'domain' ]);
    });
}

总算回到主题上来了,其实我们就是想研究是怎么注入进去的,关注bindShared方法或者是bind方法。

关键文件:\v4.2.11\vendor\laravel\framework\src\Illuminate\Container\Container.php

/**
  * 注册与容器的结合。
  *
  * @param  string|array  $abstract
  * @param  \Closure|string|null  $concrete
  * @param  bool  $shared
  * @return void
  */
public  function  bind( $abstract $concrete  = null,  $shared  = false)
 
  
1.关键的几行代码:
$concrete  $this ->getClosure( $abstract $concrete );
  
2.getClosure函数详解,这是一个闭包的函数
protected  function  getClosure( $abstract $concrete )
{
    return  function ( $c $parameters  array ())  use  ( $abstract $concrete )
    {
       $method  = ( $abstract  ==  $concrete ) ?  'build'  'make' ;
 
       return  $c -> $method ( $concrete $parameters );
    };
}
  
$this ->bindings[ $abstract ] = compact( 'concrete' 'shared' );
我们看看容器里面的对象是怎么调用了:
$value  = App::make( 'foo' );
  
Container->make()
Container->build()

 

五、IOC模式的思考和疑问

IOC,是现在很火的设计模式,就像当年的Factory和Singleton模式一样。IOC模式为我们提供了真正的松散耦合,但是松散耦合真的这么酷吗?紧耦合真的一无是处吗?不见得。
首先,使用IOC模式就必然会依赖于一些IOC容器,对于一些要求响应速度的系统而言,IOC的使用必然会降低系统性能(new 的速度肯定比Class.forName块),缓存?忘记它吧,我已经强调响应速度了。再说,IOC跟缓存也没有必然联系呀。
其次,IOC模式的大量使用会降低一些复杂模块的可读性,要知道,如果你不能写出很好的文档(多数人都是如此),那么代码就是你唯一可以与其他人沟通的语言。如果阅读代码的人不懂IOC,他如何理解你那些接口的实现?
第三,IOC容器的大量使用会造成额外的维护成本。尤其是,虽然代码中不存在耦合关系,但是耦合关系都在配置文件中,你在写出松散耦合的代码的同时也必须去写紧耦合的配置文件,对于一个大型系统而言,大量的配置文件的管理本身就必须付出高昂的代价。
最后,我认为真正意义上的松耦合是不存在的――是的,你可以“依赖”接口编程――但是毕竟还是“依赖”了。既然没有绝对的松散耦合,那么我们是否可以考虑在一定范围内使用紧耦合呢?ArrayList和Collection是紧耦合,你是否觉得不便呢,ArrayList和Iterator更是紧耦合,难道Iterator不好用吗?
考虑一下传统的工厂模式吧,它在一定程度上体现了IOC的思想,但是又没有完全的实现IOC。你可以使用工厂模式实现接口编程,但是依赖关系仍然需要在代码中体现。Hibernate是公认的优秀产品,它没有使用IOC,大量的Factory充斥其中,但是Hiberante的质量和升级速度有目共睹。也许你会说,IOC容器提供了很多底层的东西,例如缓存和对象生命周期管理等,以Spring为例,缓存倒是有,可是生命周期管理就不见得,事务管理还是需要客户介入。再说了,这些都是容器提供的,IOC模式并没有要求。之所以谈这些,主要是考虑实现一个带有缓存和“挂钩点”的工厂,这个工厂提供对象的创建和pool&cache管理但是不提供依赖关系管理。
那么IOC这个模式在哪里使用呢,我认为,应该在构件这个级别使用。构件应该是一个封装的很好的模块,它提供独立的、具有实际意义的功能。通过对构件的粒度的设计控制IOC使用的密度。而构件的内部,可以使用传统的工厂模式,也可以什么模式都不用^_^。只要提供清晰准确的接口,并且封装接口在构件内部的实现,那么即使使用public变量都没有关系!


 

名词:

闭包:http://php.net/manual/zh/functions.anonymous.php

反射:http://php.net/manual/zh/book.reflection.php

 

 

资料来源:

http://segmentfault.com/a/1190000002411255

http://segmentfault.com/a/1190000002424023

http://www.cnblogs.com/DebugLZQ/archive/2013/06/05/3107957.html

http://blog.youkuaiyun.com/realghost/article/details/35212285

### 关于 IOC 控制反转 DI 依赖注入的实验报告 #### 实验目的 通过对比不同框架中的依赖注入机制,理解控制反转IoC)与依赖注入DI)的概念及其实际应用效果。 #### 实验环境设置 为了验证 IoC DI 的工作原理,在两个不同的开发环境中进行了配置: - **ASP.NET Core**:创建了一个简单的 Web API 应用程序,并实现了基于接口的服务注册服务解析[^1]。 - **Spring Framework (Java)**:构建了一个 Java 测试项目,利用 Spring 容器管理 Bean 生命周期并演示了构造函数方式下的依赖注入过程[^2]。 #### 实验步骤说明 在 ASP.NET Core 中定义服务接口 `IAnimalService` 及其实现类 `DogService`,并通过 Startup.cs 文件内的 ConfigureServices 方法完成服务注册。对于客户端请求,则由控制器负责调用相应的业务逻辑层方法获取数据返回给前端展示。 而在 Spring 方面,编写了如下所示的单元测试案例来加载 XML 配置文件并从中取得预先声明好的 bean 对象实例[^4]: ```java import com.pg.pojo.Dog; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Test { @Test public void testDog() { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); Dog dog = context.getBean("dog", Dog.class); System.out.println(dog); } } ``` 运行上述代码后得到输出结果为: ``` Animals{name='小花', age=15} ``` 这表明成功地从容器中获得了指定名称的对象实例,并且该对象已经被正确初始化完毕可以正常使用。 #### 结果分析 无论是 .NET 还是 Java 生态圈里所采用的技术栈都证明了 IoC/DI 设计模式能够有效地降低模块间的耦合度,提高系统的可维护性扩展性。具体表现为开发者不再需要手动去创建所需的组件而是交给了专门设计用来处理这类事务的应用上下文或宿主环境去做这件事儿[^3]。 此外值得注意的是虽然两者实现细节有所差异但是核心思想是一致的即都是为了让应用程序更加灵活易于管理测试。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值