软件工程入门-轻松理解依赖注入 (DI) 和 IoC 容器

为了更好的理解依赖注入 (DI) 和 IOC 容器的概念,我们先设计一个场景。现在你饿了,准备要享用一个晚餐,那么你可能要做的事情有购买食材,烹饪食材,享用食物。

晚餐的类设计看起来应该像是这样的:

<?php
namespace Ioc;

class Dinner
{
    public function buyFood()
    {
      //
    }

    public function cookFood()
    {
      //
    }

    public function eatFood()
    {
      //
    }
}

单拿 cookFood 这步来说,你可能还需要一种能源资源,以便将食材加热,比方说,你选择了燃气。那么燃气的类设计看起来应该像是这样的:

<?php
namespace Ioc;

class Gas
{
    public function fire()
    {
        echo __CLASS__."::".__FUNCTION__.PHP_EOL;
    }
}

好了,现在可以用燃气来加热了。

...
class Dinner
{
   ...
    public function cookFood()
    {
        $gas = new Gas();
        $gas->fire();
    }
   ...
}

为节省篇幅,以上代码使用了 ‘…’ 来隐藏了部分代码,以下文章情况类似。那么调用过程是这样的:

$dinner = new \Ioc\Dinner();
$dinner->cookFood();

以上的设计就产生了依赖了,Dinner 依赖了 Gas ,这种依赖让两个类耦合在一起,这种设计的缺陷是明显的。万一燃气用光了呢,万一由天燃气改成煤气了呢,那样子晚餐就泡汤了。在代码看来就是,一旦 Gas 类在某些环境下不能运作了,一旦 Gas 要更改类名了,那么 Dinner 会很被动,况且每一次调用都要 new 实例化一次 Gas ,这很浪费系统资源。

IOC 全称是 Inversion of Control,译作控制反转。像以上设计,Dinner 称作主类, Gas 称作次类, 次类的实例化由主类来控制,这种方式就是正向的控制,如果次类的实例化并不由主类来控制的话,大概就是控制反转的意思了。

怎么解决这种强耦合关系?一种解决方式是使用工厂模式。

工厂模式

工厂模式很简单,就是使用一个代理类来帮助你批量实例化“次类”。
Agent 类如下:

<?php
namespace Ioc;

class Agent
{
    public static function useEnergy()
    {
        return new Gas();
    }
}

Dinner 类如下:

...
class Dinner
{
    protected $energy;
   ...
    public function cookFood()
    {
        $this->energy = Agent::useEnergy();
        $this->energy->fire();
    }
   ...
}

如此,即可使 Dinner 不再直接依赖 Gas,而交由一个代理 Agent 来控制 energy 的创建。然而,Gas 依赖解除了,又带来了 Agent 的依赖,虽然 Agent 的更改可能性不太,但谁能保证呢。

依赖注入 (DI)

在彻底解除依赖,必须要将次类的调用代码从主类中移除才行,否则次类像更改类名这样的改动都将牵动着所在所有依赖它的主类的代码,所有依赖它的主类都要跟着改代码,可谓牵一发而动全身。

一种依赖注入的方式就是,被依赖的对象通过参数从外部注入到类内部。更改 Dinner 类如下:

...
   public function setEnergy($energy)
   {
       $this->energy = $energy;
   }

   public function cookFood()
   {
       $this->energy->fire();
   }
...

添加一个 setEnergy 方法来注入依赖的对象。那么调用过程将变成:

$dinner = new \Ioc\Dinner();
$dinner->setEnergy(\Ioc\Agent::useEnergy());
$dinner->cookFood();

以上就是一种依赖注入的示例。Dinner 彻底解除了对能源类的依赖。

但是新问题还会产生,cookFood 并不只依赖能源,可能还依赖厨具,调味料等。那么调用过程将会是这样的:

$dinner->setEnergy(...);
$dinner->setKitchen(...);
$dinner->setSauce(...);
$dinner->cookFood();

每次都要调用很多 set 方法,这样就更不科学了。与其这样,干脆所有 set 方法都交给一个 TopAgent 做好了。
TopAgent 类如下:

<?php
namespace Ioc;

class TopAgent
{
    public static function setAllDi()
    {
        $dinner = new Dinner();
        $dinner->setEnergy(Agent::useEnergy());
        $dinner->setKitchen(Agent::useKitchen());
        $dinner->setSauce(Agent::useSauce());

        return $dinner;
    }
}

这样,调用过程就变得简单了。

到目前为止,基本上已实现了 Dinner 的依赖注入了。可认真一看,瞬间,似乎又回到了最初的问题了,不,不是似乎,简直就是了! Dinner 类是解除了外部类的依赖了,但它自己却成了 TopAgent 的依赖类了,而 TopAgent 不正是最初的 Dinner 了吗!绕了一大圈,原来还在原点,一次又一次,我们又回到了不实用的例子中来了。

一个实用和优雅的解决方法,是为依赖实例提供一个容器。即是 IOC 容器。

IOC 容器

IOC 容器首先是一种类注册器,其次它是一种更高级的依赖注入方式。它和工厂 Factory 其实性质一样,代理类,但实现机制不一样。
IOC 容器的设计模式叫做注册器模式。
Container 类如下:

<?php
namespace Ioc;

class Container
{
    protected static $objects = [];

    public static function set($key, $object)
    {
        self::$objects[$key] = $object;
    }

    public static function get($key){
        $closure = self::$objects[$key];
        return $closure();
    }
}

Agent 类再添加两个方法:

...
   public static function bindContainer()
   {
       return new Container();
   }

   public static function bindDinner(Container $container)
   {
       return new Dinner($container);
   }
...

Dinner 类接受一个 Container 注入:

<?php
namespace Ioc;

class Dinner
{
    protected $container;

    public function __construct(Container $container){
        $this->container = $container;
    }

    public function buyFood()
    {
        //
    }

    public function cookFood()
    {
        $this->container->get('energy')->fire();
    }

    public function eatFood()
    {
        //
    }
}

于是,调用过程便可漂亮的写成:

\Ioc\Container::set('energy',  function () {
    return \Ioc\Agent::useEnergy();
});

$dinner = \Ioc\Agent::bindDinner(\Ioc\Agent::bindContainer());
$dinner->cookFood();

将容器 Container 注入到 Dinner 。并实现了所有类的完全解耦。

本作品采用《CC 协议》,转载必须注明作者和本文链接
看看自己是不是一个靠谱的程序员,来做题试试。job.xyh.io
Kingmax
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 3
Haven

赞!最后面还有个巧妙的地方不知大家有没有发现:

Container绑定的时候绑定的是回调函数而不是对象的实例

\Ioc\Container::set('energy',  function () {
    return \Ioc\Agent::useEnergy();
});

在使用的时候才会去实例化这个类:

class Container
{
    public static function get($key){
        $closure = self::$objects[$key];
        return $closure();  //通过回调函数将类实例化
    }
}
4年前 评论
Kingmax (楼主) 4年前

之前一直对这两个概念不是很清楚,终于明白了,多谢分享。

4年前 评论

IOC容器的好处就是把对某个类的依赖转化为对接口的依赖

\Ioc\Container::set('energy',  function () {
    return \Ioc\Agent::useEnergy();
});

energy 就是接口,而回调函数返回的就是接口的实现,我们可以随便更改接口的实现

在需要的时候直接通过接口就可以拿到接口的实现了

 public function cookFood()
{
    $this->container->get('energy')->fire();
}

laravel的服务容器实现了通过反射获取到类的构造函数所申明的依赖,然后将类的依赖自动注入到构造函数。这样就更加方便了

4年前 评论
Kingmax (楼主) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!