Laravel 服务容器和依赖注入

当A类需要依赖于B类,也就是说需要在A类中实例化B类的对象来使用时候,如果B类中的功能发生改变,也会导致A类中使用B类的地方也要跟着修改,导致A类与B类高耦合...

依赖注入(DI)

当A类需要依赖于B类,也就是说需要在A类中实例化B类的对象来使用时候,如果B类中的功能发生改变,也会导致A类中使用B类的地方也要跟着修改,导致A类与B类高耦合。这个时候解决方式是,A类应该去依赖B类的接口,把具体的类的实例化交给外部。

就拿在业务需求中经常会用到的发送邮件举例,假如我们现在在Business业务类中需要发送邮件,下面的代码可以实现。

<?php

class Email
{
    public function send()
    {
        echo "send an email";
    }
}

class Business
{
    protected $mailer;

    public function __construct()
    {
        $this->mailer = new Email();
    }

    public function run() {
        $this->mailer->send();
    }
}

$business = new Business();
$business->run();    // send an email

上面的代码中我们先定义了一个Email类,并Business类中的构造方法中实例化了Email类的对象,然后在run方法中调用了Email实例的send方法发送邮件。到这里都很正常,但是,在现实中,需求是会改的,突然产品经理对你说发email已经过时了,现在改成发短信。

聪明的程序员在这里会问产品,以后还会不会改了,产品说,可能会。然后你就蛋疼了,你发现一改需求你的代码要改动很多,你要添加一个SmsMail类作为发短信的类,还要修改Business类,因为现在实例化的对象不是Email类了而是SmsMail类,而且以后可能还要这样地改。

这个时候,就该依赖注入上场了,我们可以写一个通用的Mail接口,让Email类和SmsMail类都实现这个接口,并在Business类中注入。

<?php 

interface Mail
{
    public function send();
}

class Email implements Mail {
    public function send() {
        echo "send an email";
    }
}

class SmsMail implements Mail {
    public function send() {
        echo "send a sms";
    }
}

class Business
{
    protected $mailer;

    public function __construct(Mail $mailer)
    {
        $this->mailer = $mailer;
    }

    public function run() {
        $this->mailer->send();
    }
}

$sms = new SmsEmail();
$email = new Email();
$business1 = new Business($sms);    // send a sms
$business2 = new Business($email);  // send an email

看到没,现在我们的Business类不用关心使用的$mailer到底是谁的实例,它可以是其他任何的Mail接口的实现。

服务容器


laravel的服务容器就是用来管理类依赖和依赖管理的一个工具。为了让A类与B类解耦,我们把B类放进一个容器里,我们只需要A类中声明它需要什么,然后由容器提供具体实例。

绑定

几乎所有服务容器的绑定都是在ServiceProviders(服务提供者)中的register方法中。基本的绑定使用$this->appbind方法,通过传递注册类或接口的名称、及返回该实例的Closure作为参数。

$this->app->singleton('Mail', function ($app) {
    return new Email();
});

还有一个singleton方法,和bind写法差不多,它们的在于在再一次请求周期中bind方法的闭包会在多次获取绑定实例的情况下执行多次,而singleton只执行一次。你也可以绑定一个已经存在的对象到容器中,$this->app->instance('request', $request);。绑定之后,我们可以通过一下几种方式来获取绑定实例:

  1. app('XblogConfig');

  2. app()->make('XblogConfig');

  3. app()['XblogConfig'];

  4. resolve('XblogConfig');

singleton绑定的好处在于如果在一次请求中我们多次使用某个类,那么只生成该类的一个实例将节省时间和空间。但是如果这个类需要在不同的环境具有“个性化”,即不同类需要不同的实例的情况下就不能使用singleton。Laravel的应用程序初始化的时候就singleton绑定Illuminate\Contracts\Http\KernelIlluminate\Contracts\Console\KernelIlluminate\Contracts\Debug\ExceptionHandler接口的实现类,这些是实现类框架的默认自带的。

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

还有一种上下文绑定,就是相同的接口,在不同的类中可以自动获取不同的实现,例如:

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

上述表明,同样的接口Filesystem,使用依赖注入时,在PhotoController中获取的是local存储而在VideoController中获取的是s3存储。