若转载教程,请注明出自SW-X框架官方文档

1、什么是依赖注入

关于 依赖注入控制反转 的概念有些人觉得很难理解,最近在给别人讲这个概念的时候梳理了一个比较好理解的解释,而且我认为非技术人员也应该能听的懂,因此分享给大家,希望下次你在给别人讲的时候也能讲的明白。

其实 依赖注入控制反转 说的是同一件事情,只是站的角度不同而已。

我们就拿超人和小怪兽的事情来做类比对象。

地球受到了威胁,不断有小怪兽来想要破坏地球,每来一个小怪兽我们就需要找一个超人去对付他,一个超人肯定是不够的,因为每次来到小怪兽都是不一样的,他们所具有的能力也是不一样的。

因此我们必须找到合适的超人去对付他,最坏的情况是每来一个小怪兽我们就要找一个或者制造一个新超人,那么来十个小怪兽,我们就要制造十个,来百个就要制造百个,来千,来万,来亿我们就要制造相应的超人,而大部分超人只能用一次。

为了解决这个问题我们引入依赖注入和控制反转的概念,我们将超人和超能力分开,独立的超人和独立的超能力,当一个小怪兽来的时候我们找到超人,将相应的超能力给予他,让他去消灭小怪兽。

这样的话我们只需要几个超人就好了,我们不再需要制造超人,而是研究如何制造更多更好的超能力给超人使用。

超能力和超人不再是强依赖关系。超能力是由外部给予超人的,超人和超能力有依赖,但是这个依赖是外部给予,因此我们可以说超能力是由外部注入给他的,所以这就叫 依赖注入

而反过来说,超人具有何种超能力不是他内部自身控制的,而是由外部控制的,相当于将超能力具有何种功效交给了外部,外部来决定超人该有的超能力,所以超能力的控制权被由自身控制反转为外部控制,这被称为 控制反转

这就是关于 依赖注入控制反转 的我的比较好理解的解释。它能较好的解决对象与对象之间的强耦合问题,同时也能做的按需使用按需加载。

顺便说一下,钢铁侠和蝙蝠侠更受欢迎的原因我认为就是因为他们的超能力是外部给予的,而非自身的,因此可以不断有新的超能力给他们使用,因此也更有看点和新鲜感。

上面部分转载至【Lravel中文社区】。

上面的部分说得可能是个概念,下面我们用通俗的实战案例来学习下什么是依赖注入控制反转

2、什么是控制反转

首先依赖注入和控制反转说的是同一个东西,是一种设计模式,这种设计模式用来减少程序间的耦合,首先我们先别追究这个设计模式的定义,否则你一定会被说的云里雾里。

先假设我们这里有一个类,类里面需要用到数据库连接,按照最最原始的办法,我们可能会这样写这个类(抛开框架的思想):

  1. class example {
  2. private $_db;
  3. function __construct(){
  4. include "./Lib/Db.php";
  5. $this->_db = new Db("localhost","root","123456","test");
  6. }
  7. function getList(){
  8. // 这里具体sql语句就省略不写了
  9. $this->_db->query("......");
  10. }
  11. }

过程:

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

看上去很完美,实现了我们想要的功能,但是这是一个噩梦的开始,项目越发壮大以后example1,example2,example3....越来越多的类需要用到db组件,如果都这么写的话,万一有一天数据库密码改了或者db类发生变化了,岂不是要回头修改所有类文件?

ok,为了解决这个问题,可能有朋友就用到了工厂模式,我们创建一个Factory方法,并通过Factory::getDb()方法来获得db组件的实例:

  1. class Factory {
  2. public static function getDb(){
  3. include "./Lib/Db.php";
  4. return new Db("localhost","root","123456","test");
  5. }
  6. }

example类就变成了:

  1. class example {
  2. private $_db;
  3. function __construct(){
  4. $this->_db = Factory::getDb();
  5. }
  6. function getList(){
  7. // 这里具体sql语句就省略不写了
  8. $this->_db->query("......");
  9. }
  10. }

千辛万苦终于又重构完了,完美!

然后我们再次想想下,假设以后example1,example2,example3....所有的类,你都需要在构造函数里通过Factory::getDb();获的一个Db实例,实际上你由原来的直接与Db类的耦合变为了和Factory工厂类的耦合,工厂类只是帮你把数据库连接信息给包装起来了,虽然当数据库信息发生变化时只要修改Factory::getDb()方法就可以了,但是突然有一天工厂方法需要改名,或者getDb方法需要改名,你又怎么办?

当然这种需求其实还是很操蛋的,但有时候确实存在这种情况,一种解决方式是:

我们不从example类内部实例化Db组件,我们依靠从外部的注入,什么意思呢?

看下面的例子:

  1. class example {
  2. private $_db;
  3. function getList(){
  4. //这里具体sql语句就省略不写了
  5. $this->_db->query("......");
  6. }
  7. //从外部注入db连接
  8. function setDb($connection){
  9. $this->_db = $connection;
  10. }
  11. }
  12. //调用
  13. $example = new example();
  14. //注入db连接
  15. $example->setDb(Factory::getDb());
  16. $example->getList();

这样一来,example类完全与外部类解除耦合了,你可以看到Db类里面已经没有工厂方法或Db类的身影了。

我们通过从外部调用example类的setDb方法,将连接实例直接注入进去。

这样example完全不用关心db连接怎么生成的了。

这就叫依赖注入,实现不是在代码内部创建依赖关系,而是让其作为一个参数传递,这使得我们的程序更容易维护,降低程序代码的耦合度,实现一种松耦合。

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

  1. $example->setDb(Factory::getDb());//注入db连接
  2. $example->setFile(Factory::getFile());//注入文件处理类
  3. $example->setImage(Factory::getImage());//注入Image处理类
  4. ...假设还有N多个依赖

这时候难道你不觉得没完没了,要写这么多个set?累不累?

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

  1. class Factory {
  2. public static function getExample(){
  3. $example = new example();
  4. $example->setDb(Factory::getDb());//注入db连接
  5. $example->setFile(Factory::getFile());//注入文件处理类
  6. $example->setImage(Factory::getImage());//注入Image处理类
  7. return $expample;
  8. }
  9. }

实例化example时变为:

  1. $example=Factory::getExample();
  2. $example->getList();

似乎完美了,但是怎么感觉又回到了上面第一次用工厂方法时的场景?

这确实不是一个好的解决方案,所以又提出了一个概念:容器,又叫做IoC容器、DI容器,控制反转。

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

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

  1. class example {
  2. private $_di;
  3. function __construct(Di &$di){
  4. $this->_di = $di;
  5. }
  6. //通过di容器获取db实例
  7. function getList(){
  8. $this->_di->get('db')->query("......");//这里具体sql语句就省略不写了
  9. }
  10. }
  11. $di = new Di();
  12. $di->set("db",function(){
  13. return new Db("localhost","root","root","test");
  14. });
  15. $example = new example($di);
  16. $example->getList();

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

这样我们只要在全局范围内申明一个Di类,将所有需要注入的类放到容器里,然后将容器作为构造函数的参数传入到example,即可在example类里面从容器中获取实例。

当然也不一定是构造函数,你也可以用一个setDi(Di $di)的方法来传入Di容器,总之约定是你制定的,你自己清楚就行。

IOC是依赖注入的精髓,而加强容器的的控制能力,可以配合PHP的反射机制,具体可以参考这篇文章:https://www.0php.net/posts/PHP-%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E5%AE%B9%E5%99%A8%E5%AE%9E%E7%8E%B0.html

免费教程手写不易,希望能支持一下SW-X框架,(^.^)

GitHub有账号的朋友,也可以给我们一个小星星噢!

希望能够与大家共同培育出良好的Swoole生态,对Swoole有兴趣的朋友可以加我微信好友,进入SW-X框架官方交流群。
该群以Swoole生态发展交流为主,若出现争吵,攻击其他人等行为,立即剔除。