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

栏目: 后端 · 发布时间: 5年前

内容简介:为了更好的理解依赖注入 (DI) 和 IOC 容器的概念,我们先设计一个场景。现在你饿了,准备要享用一个晚餐,那么你可能要做的事情有购买食材,烹饪食材,享用食物。晚餐的类设计看起来应该像是这样的:单拿 cookFood 这步来说,你可能还需要一种能源资源,以便将食材加热,比方说,你选择了燃气。那么燃气的类设计看起来应该像是这样的:

为了更好的理解依赖注入 (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 。并实现了所有类的完全解耦。


以上所述就是小编给大家介绍的《软件工程入门-轻松理解依赖注入 (DI) 和 IoC 容器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Effective Ruby:改善Ruby程序的48条建议

Effective Ruby:改善Ruby程序的48条建议

Peter J. Jones / 杨政权、秦五一、孟樊超 / 机械工业出版社 / 2016-1 / 49

如果你是经验丰富的Rub程序员,本书能帮助你发挥Ruby的全部力量来编写更稳健、高效、可维护和易执行的代码。Peter J.Jones凭借其近十年的Ruby开发经验,总结出48条Ruby的最佳实践、专家建议和捷径,并辅以可执行的代码实例。 Jones在Ruby开发的每个主要领域都给出了实用的建议,从模块、内存到元编程。他对鲜为人知的Ruby方言、怪癖、误区和强力影响代码行为与性能的复杂性的揭......一起来看看 《Effective Ruby:改善Ruby程序的48条建议》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具