[看完就懂] Laravel 服务容器,IoC,DI

DI#

DI 就是常说的依赖注入,那么究竟什么是依赖注入呢?

打个比方,电脑(非笔记本哈)需要键盘和鼠标我们才能进行操作,这个‘需要’换句话说就是‘依赖’键盘和鼠标。

那么,相应的,一个类需要另一个类才能进行作业,那么这也就是依赖。

看一段代码:


    class Computer {
        protected $keyboard;

        public function __construct() {
            $this->$keyboard = new Keyboard();
        }
    }

    这里的Computer类依赖了键盘类。

好,既然我们已经知道了什么是依赖,那么什么是注入呢?

我们改造一下上面的代码:

     class Computer {
        protected $keyboard;

        public function __construct(Keyboard $keyboard) {
            $this->$keyboard = $keyboard;
        }
    }

    $computer = new Computer(new Keyboard());


    这里的Computer类依赖注入了Keyboard类。

关于依赖注入,我的理解是:

所需要的类通过参数的形式传入的就是依赖注入。

理解了依赖注入,我们可以接着理解 IOC。

IOC#

IOC 是什么呢?

中文叫控制反转。啥意思呢? 这个看明白了 DI 后就能很容易的理解了。

通过 DI 我们可以看到,一个类所需要的依赖类是由我们主动实例化后传入类中的。

控制反转和这个有什么关系呢?

控制反转意思是说将依赖类的控制权交出去,由主动变为被动。

看一段 laravel 代码:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SessionController extends Controller
{

    public function login(Request $request)
    {
        //这就是IOC,我们不需要主动传入类了一切由laravel去实现
    }
}

看到这你可能有疑问了,这是怎么实现的呢?

这就是靠服务容器了,请往下接着看。

服务容器#

看了很多文章,我一致认为服务容器就是一种设计模式。

它的目的就是解耦依赖。

它有点类似于我前面说的《享元模式》。区别在于服务容器解决了所有依赖的实现。

这里我们再从头至尾的看一遍,怎么一步步演化出服务容器。

依然是电脑的例子,我们知道电脑依赖键盘鼠标,可是键盘鼠标也有很多种呀。

先看一个最原始的代码例子:


     class Computer {
        protected $keyboard;

        public function __construct($type = null) {

            switch($type) {
                case 'common':
                    $this->keyboard = new CommonKeyboard();
                case 'awesome':
                    $this->keyboard = new AweSomeKeyboard();
                default:
                    $this->keyboard = new Keyboard();
            }

        }
    }

或许你一眼就看出了问题在哪。

如果我们又要增加一种键盘,那我们又得对这个类进行修改。这样下去,这个类会变得庞大且耦合程度过高。

那么我们可以怎么修改呢?

  • 工厂模式

这样我们可以避免直接的修改 Computer 类。

    简单工厂
    class Factory {

        public static function getInstance($type){
            switch($type) {
                case 'common':
                    $this->keyboard = new CommonKeyboard();
                    break;
                case 'awesome':
                    $this->keyboard = new AweSomeKeyboard();
                    break;
                default:
                    $this->keyboard = new Keyboard();
                    break;
            }
        }
    }

    class Computer {
        protected $keyboard;

        public function __construct($type == null) {
            $this->keyboard = Factory::getInstance($type);
        }
    }

这样使用简单工厂模式后,我们后续的修改可以不用对 Computer 类进行操作而只要修改工厂类就行了。这就相当于对 Computer 类进行了解耦。

Computer 类虽不在依赖那些键盘类了,但是却变为依赖工厂类了。

后续添加新类型的键盘就必须对工厂类进行修改。

所以这个工厂类还不能很好的满足要求,我们知道电脑对键盘的接口都是一致的,键盘必须实现这一接口才能被电脑识别,那我们对 Computer 和 Keyboard 类进行修改。

  • DI(依赖注入)
    interface Board {
        public function type();
    }

    class CommonBoard implements Board {
        public function type(){
            echo '普通键盘';
        }
    }

    class MechanicalKeyboard implements Board {
        public function type(){
            echo '机械键盘';
        }
    }

    class Computer {
        protected $keyboard;

        public function __construct (Board $keyboard) {
            $this->keyboard = $keyboard;
        }
    }

    $computer = new Computer(new MechanialKeyBoard());

可是这样也有问题,如果我们后续对这台电脑使用的键盘不满意要进行替换呢? 我们又回到原点了,必须去修改传入的键盘类。

能不能做成可配置的呢?

  • IOC 服务容器(超级工厂)
class Container
{
    protected $binds;

    protected $instances;

    public function bind($abstract, $concrete)
    {
        if ($concrete instanceof Closure) {
            $this->binds[$abstract] = $concrete;
        } else {
            $this->instances[$abstract] = $concrete;
        }
    }

    public function make($abstract, $parameters = [])
    {
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        array_unshift($parameters, $this);

        return call_user_func_array($this->binds[$abstract], $parameters);
    }
}

这就是一个简单的 IOC 服务容器。

这个怎么解决我们上述的问题呢?

    $container = new Container;

    $container->bind('Board', function($container){
        return new CommonBoard;
    });

    $container->bind('Computer',function($container,$module){
        return new Computer($container->make($module));
    });

    $computer = $container->make('Computer',['Board']);

这里生产出来的 Computer 类就是一个使用普通键盘的电脑类了。

解释一下代码:

    bind(name,function($container){
        return new Name;
    })

    这里的name和Name之间的关系是:
    当我需要name类的时候你就给我实例化Name类。

    make(name)方法是对name进行生产返回一个实例。

如果我们要更换键盘怎么办呢?

    $container->bind('Board', function($container){
        return new MechanicalBoard;
    });

    $container->bind('Computer',function($container,$module){
        return new Computer($container->make($module));
    });

    $computer = $container->make('Computer',['Board']);

只要对 bind 绑定的 Board 类的实现进行修改,我们就可以很容易替换掉键盘了。这就是一个服务容器。

对服务容器进行一个理解:

容器就是一个装东西的,好比碗。而服务就是这个碗要装的饭呀,菜呀,等等东西。当我们需要饭时,我们就能从这个碗里拿到。如果你想在饭里加点菜(也就是饭依赖注入了菜),我们从碗里直接拿饭就可以了,而这些依赖都由容器解决了(这也就是控制反转)。

我们需要做的就是对提供的服务进行维护。

我们看一段真实的在 laravel 框架上能跑的代码:

代码

当然 laravel 框架的服务容器比这里的要复杂很多了,但我们明白了它的使用目的以及使用场景就不难去入手 laravel 了。

PS:你的赞是我创作的动力!

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 6年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 50
wanghan

要是还没看懂怎么办,还有的救吗

6年前 评论
xujinhuan 4年前

我就喜欢这种深入浅出的文章

6年前 评论

@雪风 恭喜首评:smile:,献上香吻一个:kissing:

6年前 评论
wanghan

要是还没看懂怎么办,还有的救吗

6年前 评论
xujinhuan 4年前

依赖注入一般是靠 PHP 的反射类来实现的,可以实现无限级依赖的注入。

6年前 评论

非常棒

6年前 评论

https://implode.io/4mT8O4 这个网站不错,还能直接运行 laravel 代码

6年前 评论
QIN秦同学

$container = new $container;
是否应为
$container = new Container;

6年前 评论
QIN秦同学

$container->bind('Computer',function($container,$module){
return new Computer($container->make($module));
});
求问:$module 参数 指什么。
看不太懂这一步。求解释。

6年前 评论
QIN秦同学

我还是有点不理解。哈哈。好像又发现了一个错误的地方。
$computer = $container->make('Computer','Board');
应为
$computer = $container->make('Computer',['Board']);
私信楼主了。怎么添加个联系方式,求问下。

6年前 评论

@echofree313 这里只要理解为什么要有服务容器以及它的应用场景就好了,技术细节还要参考其他书籍

6年前 评论
QIN秦同学

其实 我是不理解 绑定 computer。绑定键盘类是理解的。绑定 computer 时候,里面的 $container->make ($module),不理解运行过程,可以给个具体的过程分析下么。这个貌似运行不成功吧?有点菜鸡。求指教。

6年前 评论

@echofree313 可以运行的,用 debug 工具对每一步打下断点就可以明白运行流程了。

6年前 评论

楼主,服务容器的基本原理知道了,但在 Laravel 中怎么具体应用的,能举个例子吗?
比如,控制器中的非构造方法是怎么实现自动注入的,如果能有一篇文分析下完整过程,那就太棒了。

class SessionController extends Controller
{

    public function login(Request $request)
    {
        //这就是IOC,我们不需要主动传入类了一切由laravel去实现
    }
}
6年前 评论

@hehorange 好的,争取在年前写一篇相关的文章:smile:

6年前 评论

@simpleT ↖(^ω^)↗期待,先点赞了

6年前 评论

Factory 类里面 static 方法不能用 this 吧。。。。。而且并没有 return

6年前 评论

解释的很好, 清晰易懂

6年前 评论

眼前一亮,依赖 和依赖 注入 ,容器 有加深了一点印象。
台式电脑 需要 键盘 依赖 (内部 依赖 外部 没有 键盘 打不字)
不同键盘 注入 到 电脑( 生活有了改变,可以用上了 好的键盘 ,有了这个给外部的键盘注入进来)

6年前 评论
EthanYep

有所收获

6年前 评论

file

这里是不是应该是一个等号?

6年前 评论

Laravel 容器中,依赖解析的关键代码是:https://github.com/laravel/framework/blob/... ,使用类反射并不断递归,实现 依赖的 [依赖的][依赖的][...] 自动解析。

6年前 评论

论坛很多关于这个概念的文章,这篇是我个人觉得最棒的。

6年前 评论

非常感谢楼主的文章,深入浅出。

6年前 评论

@shawn805 我也看了这篇文章,思路一样,不知道是谁先写谁后写

6年前 评论

@shawn805 我也看了这篇文章,思路一样,不知道是谁先写谁后写

6年前 评论

@shawn805 这里的是简化修改版,原文一系列笔记真不错

6年前 评论

file
这个哪来的呢?

5年前 评论

:thumbsup: 比喻贴切,导入也清晰。加深了我对依赖注入、服务容器的理解,感谢分享。

5年前 评论

刚又看了遍,服务容器举例中

file。是不是应该这样写
$this->$keyboard = new 键盘类?

5年前 评论

@维 C 哥哥我只接受程序嫒的香吻,秃顶兄弟走开。

5年前 评论

file
这里打错了

5年前 评论
wanghan

composer 类注入 Board $keyboard 这里看不懂,这个 Board 不是 interface 吗,依赖注入还可以注入 interface 啊?

5年前 评论
Code_Er

搂主可以列举一点实例来对比一下用了服务容器的好处吗 这样我感觉会比较深刻一点

5年前 评论

对比了楼主的举例,依赖注入 和 服务容器, 依赖注入要更换键盘这样既可: $computer = new Computer (new MechanialKeyBoard ()); $computer = new Computer (new CommonBoard ()); 而服务容器: $container->bind ('Board', function ($container){ return new MechanicalBoard; }); $container->bind ('Board', function ($container){ return new CommonBoard; }); 同样只要修改一处地方,这不完全没区别吗?服务容器的代码还更加复杂难懂; 服务容器的优势应该不是这里吧?楼主的举例好像没能说明服务容器的优势 楼主的说明通俗易懂,但麻烦楼主再斟酌下优势的说明,这样一对比我没看到服务容器的优势

5年前 评论
okulamatata 4年前

生成容器了,怎么使用呢,调用方法

5年前 评论