控制反转与依赖注入 (2) - 实现一个简单 IoC 容器来理解依赖自动注入的好处

未匹配的标注

手握设计模式宝典 - 控制反转与依赖注入 (2) - 实现一个简单 IoC 容器来理解依赖自动注入的好处

Design-Pattern - Inversion-Of-Control and Dependency-Injection

  • Title: 《手握设计模式宝典》 之 控制反转与依赖注入 (2) - 实现一个简单 IoC 容器来理解依赖自动注入的好处
  • Tag: Design-PatternInversion-Of-ControlDependency-InjectionIoCDI设计模式
  • Author: Tacks
  • Create-Date: 2023-10-17
  • Update-Date: 2023-10-17

大纲

0、Series

0、REF

0、Keyword

  • Object Oriented 面向对象
  • Dependency Inversion Principle 依赖倒置原则 DIP
  • Simple Factory Pattern 简单工厂模式
  • Inversion of Control 控制反转 IoC
  • Dependency Injection 依赖注入 DI
  • Dependent Class 依赖类
  • Dependency 依赖项
  • Container 容器
  • Inversion of Control Container IOC 容器

1、 概念

IoC容器

1.1 控制反转

『控制反转』IoC面向对象编程中的一种设计原则,通过反转应用程序的控制流以实现其类之间的松耦合。IoC 是关注强制隔离。

看『控制反转』一词可以拆分,”控制” 和 “反转”。什么控制?也就是应用程序的控制流,执行流;什么是反转?控制流发生反转。“反转” 相对于早期过程化编程,由框架来将控制流进行反转,例如实例化对象、处理依赖关系、管理对象生命周期、等等,这些控制流都将隐藏于框架之中,我们编写的业务代码将要运行在现代化框架之上。控制反转带来的效果就是:框架调用应用程序代码应用程序代码调用类库。应用程序代码因为控制反转,更解耦更灵活。我们定义好组件、接口、依赖关系,然后框架根据配置或者注解等手段来实现自动实例化对象,并将依赖关系注入到对象之中。

控制反转 使得框架或容器负责控制、管理应用程序的对象和执行流,将应用程序代码解耦并聚焦于业务逻辑的编写。

通常想要实现 控制反转 ,会采用 依赖注入 的设计模式,当然还有其他方法,如依赖查找、服务定位器模式等。

1.2 依赖注入

『依赖注入』 DI,是控制反转设计原则的一种具体设计模式。旨在分离构造对象和使用对象的关注点,从而导致松散耦合的程序。 DI 是关注如何注入依赖项。

通常想要实现 依赖注入,由三种方式、构造函数注入、setter注入、接口注入。

2、实践

2.1 使用 OOP 编写用户类和咖啡机类,完成喝咖啡的场景

2.1.1 咖啡机类定义

  • Interface CoffeeMachineInterface
    • 一个咖啡机的核心功能就是 makeCoffee($coffeeBeans)
namespace App\More\Di\Coffee;

interface CoffeeMachineInterface {
    public function makeCoffee($coffeeBeans);
}
  • Class AmericanoCoffee
    • 提供一个美式功能的咖啡机
    • 实现 接口约束,提供 makeCoffee($coffeeBeans) 功能
namespace App\More\Di\Coffee;

class AmericanoCoffee implements CoffeeMachineInterface {
    public function makeCoffee(string $beans)
    {
        echo sprintf("[%s] ===> 开始制作", date('Y-m-d H:i:s')) . PHP_EOL;
        $this->getEspresso($beans);
        $this->getWater();
        echo sprintf("[%s] ===> 制作完成", date('Y-m-d H:i:s')) . PHP_EOL;
    }

    public function getEspresso(string $beans)
    {
        echo sprintf("[%s] 得到({%s})意式浓缩", __CLASS__ , $beans) . PHP_EOL;
    }

    public function getWater()
    {
        echo sprintf("[%s] 加水", __CLASS__) . PHP_EOL;
    }
}
  • Class LatteCoffee
    • 提供一个拿铁功能的咖啡机
    • 实现 接口约束,提供 makeCoffee($coffeeBeans) 功能
namespace App\More\Di\Coffee;

class LatteCoffee implements CoffeeMachineInterface {
    public function makeCoffee(string $beans)
    {
        echo sprintf("[%s] ===> 开始制作", date('Y-m-d H:i:s')) . PHP_EOL;
        $this->getEspresso($beans);
        $this->getMilk();
        echo sprintf("[%s] ===> 制作完成", date('Y-m-d H:i:s')) . PHP_EOL;
    }

    public function getEspresso(string $beans)
    {
        echo sprintf("[%s] 得到({%s})意式浓缩", __CLASS__ , $beans) . PHP_EOL;
    }

    public function getMilk()
    {
        echo sprintf("[%s] 加奶", __CLASS__) . PHP_EOL;
    }
}

2.1.2 用户类定义

  • Class User
    • 提供用户类,有喝咖啡的功能 drinkCoffee()
class User
{
    protected $coffeeMachine;

    public function __construct()
    {
        $this->coffeeMachine = new AmericanoCoffee(); 
    }

    public function drinkCoffee(string $beans)
    {
        $this->coffeeMachine->makeCoffee($beans);

        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}

2.1.3 用户喝咖啡

  • 用户类 User 和 美式咖啡机类 AmericanoCoffee 都有了,调用一下试试看
$tacks = new User();
$tacks->drinkCoffee("阿拉比卡");
// 结果如下
[2023-10-13 06:41:50] ===> 开始制作
[App\More\Di\Coffee\AmericanoCoffee] 得到({阿拉比卡})意式浓缩
[App\More\Di\Coffee\AmericanoCoffee] 加水
[2023-10-13 06:41:50] ===> 制作完成
[App\More\Di\Coffee\User] 开喝!

2.1.4 新的问题-如何灵活喝咖啡

问题产生:用户想灵活喝咖啡-简单工厂模式?

执行起来也是非常顺畅…,但是一旦有一天你不想喝美式,想喝拿铁了,是不是就要更换机器, 而这句的 $this->coffeeMachine = new AmericanoCoffee();User 类之间形成了硬编码依赖,修改起来就没那么灵活了。 也就是说你想喝拿铁就必须修改 User 类,比如下面这几种情况。

  • 解决方案:简单工厂模式 (Simple Factory Pattern)
    • 新增字段来区分使用什么机器
    • 基本上改的 User 类完全不一样了
    • drinkCoffee(string $beans) 改成了 drinkCoffee(string $type, string $beans)
    • 新增了 getMachine($type) 类似简单工厂来获取咖啡机

但是可以看出来,压力给到 $type 字段,并且 咖啡机类,还是和 User 类耦合在一起,如果有新的咖啡机,那么我将依然需要修改 User

class User1
{
    protected $coffeeMachine;

    public function getMachine($type) {
        if ($type == '美式') {
            $this->coffeeMachine = new AmericanoCoffee(); 
        } else if ($type == '拿铁') {
            $this->coffeeMachine = new LatteCoffee(); 
        }
    }

    public function drinkCoffee(string $type, string $beans)
    {
        // 设置机器
        $this->getMachine($type);
        // 制作咖啡
        $this->coffeeMachine->makeCoffee($beans);

        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}
$tacks = new User1();
$tacks->drinkCoffee("拿铁", "阿拉比卡");

/*
[2023-10-13 07:22:48] ===> 开始制作
[App\More\Di\Coffee\LatteCoffee] 得到({阿拉比卡})意式浓缩
[App\More\Di\Coffee\LatteCoffee] 加奶
[2023-10-13 07:22:48] ===> 制作完成
[App\More\Di\Coffee\User1] 开喝!
*/

2.2 利用 依赖注入 DI,来解决用户类和咖啡机类太过耦合的问题

『依赖注入』 来解决用户类和咖啡机类耦合问题,灵活切换咖啡类型,让对象的创建和使用分离 是关键。

2.2.1 构造函数

依赖注入-构造函数注入

  • User
class UserA
{
    protected $coffeeMachine;

    public function __construct(CoffeeMachineInterface $machine)
    {
        $this->coffeeMachine = $machine; 
    }

    public function drinkCoffee(string $beans)
    {
        $this->coffeeMachine->makeCoffee($beans);

        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}
  • 喝咖啡

得益于依赖的分离设计,在不改变 User 类的情况下,你想喝美式、拿铁都可以,因为他们都实现了 CoffeeMachineInterface 接口,符合咖啡机的契约,可以被实例化后注入。

/*
// 之前调用方式,User内部确定使用什么类型的咖啡机
$tacks = new User();
$tacks->drinkCoffee("阿拉比卡");
 */

// 依赖注入方式,调用喝咖啡的过程稍微麻烦一些
$tacks = new UserA(new LatteCoffee());
$tacks->drinkCoffee("阿拉比卡");

$tacks = new UserA(new AmericanoCoffee());
$tacks->drinkCoffee("曼特宁");

2.2.2 Setter

依赖注入-setter设置注入

  • User
class UserB
{
    protected $coffeeMachine;

    public function setter(CoffeeMachineInterface $machine) {
        $this->coffeeMachine = $machine;
    }

    public function drinkCoffee(string $beans)
    {
        $this->coffeeMachine->makeCoffee($beans);

        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}
  • 喝咖啡

由于 User 其实和 CoffeeMachineInterface 并不是强依赖,也就是说如果用户必须要天天喝咖啡,那么可以利用构造函数注入,反之也可以用 setter 灵活来注入所需对象。

$tacks = new UserB();
$tacks->setter(new LatteCoffee());
$tacks->drinkCoffee("Arabica");

2.2.3 接口注入

依赖注入-接口注入

其实类似 setter , 只是实现方式上稍微不同,还增加了一个约束接口。

interface DependencyInterface {
    public function injectDependency(CoffeeMachineInterface $dependency);
}
class UserC implements DependencyInterface
{
    protected $dependency;

    public function injectDependency(CoffeeMachineInterface $dependency) {
        $this->dependency = $dependency;
    }

    public function drinkCoffee(string $beans)
    {
        $this->dependency->makeCoffee($beans);

        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}


$tacks = new UserC();
$tacks->injectDependency(new LatteCoffee());
$tacks->drinkCoffee("Arabica");

2.2.4 新的问题-如何实现参数自动解析无需手动 new ?

至此,我们再来看一下什么叫做 『依赖注入』 ?

在面向对象设计中,由于类之间会有依赖关系,那么如果依赖类 User 中直接实例化依赖项 LatteCoffee 或者 AmericanoCoffee,那么就产生耦合关系。如果我们通过一种手段,如构造函数注入、setter注入、接口注入等方式将 内部实例化的过程,改为在外部实例化后进行注入,就实现了 DI 依赖注入。

问题产生 : 如何实现依赖自动注入?

依赖注入是好,但是每次调用的时候,要明确的自行实例化,如果构造函数有参数,还需要去查看所需参数,稍微步骤多了一些,能不能再简化简化?

  • 问题答案:反射

2.3 利用 反射,来省去主动实例化依赖项的过程

2.3.1 通过反射实现一个简单的自动注入类-动态解析所需参数对象

  • 一个新的 User 类 UserAutoA
class UserAutoA
{
    public function drinkCoffee(LatteCoffee $machine, string $beans)
    {
        $machine->makeCoffee($beans);
        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}

如果想要实例化的话,可以如下,但是我们发现,这样也没办法解决需要主动 new 依赖项,还是会看到很多 new

(new UserAutoA())->drinkCoffee(new LatteCoffee(), '阿拉比卡');

然后我简单实现了一种方式,来大概实现自动注入依赖项的过程。 虽然有些简陋,并且并不适合生产,只是方便理解如何进行自动注入,主要就是依靠反射 !。

  • ReflectionMethod 利用反射获取方法的信息
  • ReflectionNamedType 利用反射获取字段类型的信息
  • ReflectionClass 利用反射获取类的信息
  • call_user_func_array() 利用回调函数传入方法参数并执行
class AppAuto
{
    public static function run($instance, $method, $parameters)
    {
        // 方法是否存在
        if (!method_exists($instance, $method)) {
            return null;
        }

        $reflector = new \ReflectionMethod($instance, $method);

        // 获取参数
        foreach ($reflector->getParameters() as $key => $parameter) {
            // var_dump($key);
            // var_dump($parameter);
            // var_dump($parameter->getType());

            // 获取参数类型
            $parameterType = $parameter->getType();
            assert($parameterType instanceof \ReflectionNamedType);
            // 获取参数名
            $parameterTypeName  = $parameterType->getName();

            if(class_exists($parameterTypeName)) {
                $reflectorClass = new \ReflectionClass($parameterTypeName);
                if(!$reflectorClass->isInstantiable()) {
                    echo sprintf("[%s] 无法实例化的类:%s", __CLASS__, $parameterTypeName) . PHP_EOL;
                }

                // 获取构造函数
                $constructor = $reflectorClass->getConstructor();

                // 如果没有构造函数,没有依赖,可以直接进行实例化
                if (is_null($constructor)) {
                    // 数组中插入指定元素
                    array_splice($parameters, $key, 0, [
                        new $parameterTypeName
                    ]);
                } else {
                    echo sprintf("[%s] 构造函数中可能有依赖:%s", __CLASS__, $parameterTypeName) . PHP_EOL;
                }
            } else {
                // echo sprintf("[%s] 不存在的类:%s", __CLASS__, $parameterTypeName) . PHP_EOL;
            }
        }
        call_user_func_array([
            $instance,
            $method
        ], $parameters);
    }
}

而调用方式如下,可以看到,没有手动注入依赖项 LatteCoffee,也能实现调用。

AppAuto::run(new UserAutoA(), 'drinkCoffee', ['咖啡豆']);

2.3.2 新的问题

问题产生 : 如何解决依赖自动注入,并实现依赖类和依赖项的解耦?

可以看到 UserAutoA 类的方法定义为 public function drinkCoffee(LatteCoffee $machine, string $beans) ,此时我们看到,定义的时候就是明确了 LatteCoffee 拿铁咖啡机类,如果说我想要美式咖啡机类,是不是又要换一种定义方法 public function drinkCoffee(AmericanoCoffee $machine, string $beans) ?

这并没有给我们带来灵活的注入,反而增加了成本,甚至不如简单工厂模式的方式,根据不同类型来进行咖啡机的实例化。
而且,这也并没有解决依赖的关系,依然是 LatteCoffeeUserAutoA 依赖在一起。

这里可以了解一下 DIP (Dependency Inversion Principle) 依赖反转原则,或者说依赖倒置。

DIP
高层模块(high-level modules)不要依赖低层模块(low-level);
高层模块和低层模块应该通过抽象(abstractions)来互相依赖,也就是通过接口或者抽象类,实现面向接口编程;
抽象(abstractions)不要依赖具体实现细节(details);
具体实现细节(details)依赖抽象(abstractions);

  • 依赖?
    • 用户喝咖啡,需要依赖咖啡机,这就是依赖
  • 高层和底层?
    • 用户是高层
    • 拿铁咖啡机是底层/美式咖啡机是底层
  • 高层模块不要依赖底层模块,应该依赖抽象标准?
    • 用户是高层,如果想要喝咖啡,美式拿铁都可以,难道要搞两个喝咖啡的动作方法吗?
    • 所以用户依赖的是咖啡机做咖啡的功能,makeCoffee() ,只要能做咖啡的机器,都能让用户喝到咖啡
    • 所以用户高层应该依赖,CoffeeMachineInterface 接口层,只要满足 makeCoffee() 方法就行

所以,UserAutoA 高层用户,下面两种喝咖啡方法定义

  • public function drinkCoffee(AmericanoCoffee $machine, string $beans)
  • public function drinkCoffee(LatteCoffee $machine, string $beans)

都是没能解决依赖的问题,而应该定义成

  • public function drinkCoffee(CoffeeMachineInterface $machine, string $beans)

问题产生 : 依赖项是接口,无法被实例化,依赖注入如何确定实例化对象?

这就不得不说,需要用到 IoC 容器 ,由于抽象接口 CoffeeMachineInterface 无法被实例化,需要利用 bind() 绑定之类的手段,或者上下文绑定,将接口绑定实例,来确定你是喝美式 AmericanoCoffee,还是喝拿铁 LatteCoffee 的实例化。只有确定后,才能知道依赖注入需要的依赖项。

  • 问题答案:IoC容器的 bind()

2.4 利用 IoC 容器,将依赖项的接口绑定到具体实例上

容器会保存所有我们需要依赖的对象。

2.4.1 实现一个简单的IoC容器-主要侧重绑定 bind 功能

  • Container
    • bind($abstract, $concrete) 绑定
    • make($abstract, $paramters = []) 实例化
  • 依靠两个数组
    • 数组1 $binds 存储绑定关系,类名对应的回调函数或者对象
    • 数组2 $instances 存储对象,实例化之后的对象
namespace App\More\Di\Coffee;

/**
 * 容器 Container
 */
class Container 
{
    protected $binds;
    protected $instances;

    /**
     * 绑定
     *
     * @param string $abstract
     * @param mixed $concrete (闭包/实例化对象)
     * @return mixed
     */
    public function bind($abstract, $concrete)
    {
        if ($concrete instanceof \Closure) {
            // 如果是闭包,直接绑定
            $this->binds[$abstract] = $concrete;
        } else {
            $this->instances[$abstract] = $concrete;
        }
    }

    /**
     * 实例化
     *
     * @param string $abstract
     * @param array $paramters
     * @return mixed
     */
    public function make($abstract, $paramters = [])
    {
        if(isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        // 追加到第一个参数
        array_unshift($paramters, $this);

        // 执行闭包 Closure 延迟获取对象
        return call_user_func_array($this->binds[$abstract], $paramters);
    }
}

2.4.2 用户喝咖啡方法中注入

  • Class User1
    • drinkCoffee(CoffeeMachineInterface $machine, string $beans)

用户喝咖啡方法中,需要传入一个咖啡机实例,由于这个是接口类型 CoffeeMachineInterface ,只要满足这个接口的类的实例化对象都行

class User1
{
    public function drinkCoffee(CoffeeMachineInterface $machine, string $beans)
    {
        $machine->makeCoffee($beans);
        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}

目前满足 CoffeeMachineInterface 接口类型的有两个实现

  • AmericanoCoffee
  • LatteCoffee

具体调用如下

// 先进行绑定
$IOC = new Container();
$IOC->bind(AmericanoCoffee::class, function() {
    return new AmericanoCoffee();
});
$IOC->bind(LatteCoffee::class, function() {
    return new LatteCoffee();
});

$IOC->bind(User1::class, function() {
    return new User1();
});

echo "===========================================================[User1]" . PHP_EOL;

// 实例化
$coffeeA = $IOC->make(LatteCoffee::class);
$userA   = $IOC->make(User1::class);

// 依赖注入 coffeeA
$userA->drinkCoffee($coffeeA, '耶加雪啡');

/*
[2023-10-17 07:20:35] ===> 开始制作
[App\More\Di\Coffee\LatteCoffee] 得到({耶加雪啡})意式浓缩
[App\More\Di\Coffee\LatteCoffee] 加奶
[2023-10-17 07:20:35] ===> 制作完成
[App\More\Di\Coffee\User1] 开喝!
*/

2.4.3 用户构造函数中注入

  • Class User2
    • __construct(CoffeeMachineInterface $machine)
    • drinkCoffee(string $beans)
class User2
{
    protected $coffeeMachine;

    public function __construct(CoffeeMachineInterface $machine)
    {
        $this->coffeeMachine = $machine;
    }

    public function drinkCoffee(string $beans)
    {
        $this->coffeeMachine->makeCoffee($beans);

        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}
  • 具体调用如下
// 先进行绑定
$IOC = new Container();
$IOC->bind(AmericanoCoffee::class, function() {
    return new AmericanoCoffee();
});
$IOC->bind(LatteCoffee::class, function() {
    return new LatteCoffee();
});

$IOC->bind(User1::class, function() {
    return new User1();
});

echo "===========================================================[User2]" . PHP_EOL;

$IOC->bind(User2::class, function($container, $coffee) {
    return new User2($container->make($coffee));
});

// 实例化
$userB   = $IOC->make(User2::class, [AmericanoCoffee::class]);
$userB->drinkCoffee('瑰夏');

/*
[2023-10-17 07:20:35] ===> 开始制作
[App\More\Di\Coffee\AmericanoCoffee] 得到({瑰夏})意式浓缩
[App\More\Di\Coffee\AmericanoCoffee] 加水
[2023-10-17 07:20:35] ===> 制作完成
[App\More\Di\Coffee\User2] 开喝!
*/

2.4.4 如何上下文绑定?

上面 $IOC->make(User2::class, [AmericanoCoffee::class]); 的例子,上下文条件是 User2::class,依赖项是 CoffeeMachineInterface 接口,需要进行上下文绑定。用户想喝什么样的咖啡,这通常在你早上的时候会确定(也就是应用配置中),例如将 AmericanoCoffee::class 作为依赖项接口 CoffeeMachineInterface 的具体实现。

具体上下文绑定这块,可以参考 Laravel 的实现。

  • 类似 Laravel 服务容器 中如下代码 -> 文档
    • when() 方法来指定上下文条件
    • needs() 方法来定义依赖项的抽象目标
    • give() 方法来定义上下文绑定的具体实现

上下文条件是 [VideoController::class, UploadController::class] ,表示当解析 VideoControllerUploadController 类时,应用下面的上下文绑定规则。依赖项的抽象目标是 Filesystem::class,表示要为 Filesystem 接口进行上下文绑定。

具体的实现使用了匿名函数,函数内部返回了 Storage::disk('s3'),也就是使用 S3 存储磁盘作为 Filesystem 接口的具体实现。这意味着当解析 Filesystem 依赖项时,容器将返回 S3 存储磁盘的实例 。

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

综上所属,IoC 容器,主要有几点需要重点理解,依赖注入、自动实例化依赖项、IoC容器中接口绑定具体实现;

3、IoC 简易实现

  • DI 依赖注入
    • 依赖关系定义在外部而不是内部自行实例化,分离依赖项对象的创建和使用
    • 通过 User 的构造函数进行注入 __construct(CoffeeMachineInterface $coffeeMachine)
    • 依赖项 $coffeeMachine 是通过外部实例化提供的
    • 好处:解耦,易于扩展。易于测试;
  • IoC 容器
    • IoC 实现依赖注入,负责依赖关系的绑定和对象创建创建,
  • 接口绑定具体实现
    • IoC bind() 绑定方法,将接口与具体实现类进行绑定,这样在用到接口的地方,容器才会自动提供与该接口绑定的具体实现类的实例。
  • 自动实例化依赖项
    • IoC make() 方法,利用反射机制 Reflection,获取依赖类的构造函数参数,并根据绑定关系实例化对应的依赖对象。
class User
{
    protected $coffeeMachine;

    public function __construct(CoffeeMachineInterface $coffeeMachine)
    {
        $this->coffeeMachine = $coffeeMachine;
    }

    public function drinkCoffee(string $beans)
    {
        $this->coffeeMachine->makeCoffee($beans);

        echo sprintf("[%s] 开喝!", __CLASS__) . PHP_EOL;
    }
}

class Container
{
    private $binds = [];

    public function bind($contract, $concrete)
    {
        $this->binds[$contract] = $concrete;
    }

    public function make($className)
    {
        $reflectionClass = new \ReflectionClass($className);
        $constructor = $reflectionClass->getConstructor();
        $parameters = $constructor->getParameters();
        $args = [];

        foreach ($parameters as $param) {
            // 获取参数类型
            $parameterType = $param->getType();
            assert($parameterType instanceof \ReflectionNamedType);
            // 获取参数名
            $parameterTypeName  = $parameterType->getName();

            $paramInstance = new $this->binds[$parameterTypeName]();
            $args[] = $paramInstance;
        }

        // 实例化
        return $reflectionClass->newInstanceArgs($args);
    }
}

// IOC 容器
$IOC = new Container();
// 绑定
$IOC->bind(CoffeeMachineInterface::class, LatteCoffee::class);
// 实例化
$user = $IOC->make(User::class);
// 执行
$user->drinkCoffee("埃塞和比亚");

依赖自动注入的好处

  • 解耦合
    • 依赖自动注入,减少类之间直接依赖关系
    • 自动让你只需要声明所依赖的接口,具体实现和创建过程由 IoC 容器负责
  • 简化代码
    • 省去传递依赖对象中的手动 new 实例化。交给 IoC 容器来完成
  • 易于测试
    • 依赖注入可以更好的 mock 依赖对象
  • 易于扩展
    • 更加灵活,把依赖关系委托给 IoC 容器,更易于替换
    • 例如同一个接口,有多种不同的实现,可以在配置文件中指明具体用哪种实现,比如 Laravel 中的 Filesystem 文件存储系统

一句话总结

IoC 容器,就是一个容器类,里面保存各种我们需要的对象,并且实现依赖注入,帮助我们解决类之间的依赖问题。通过绑定确定接口依赖项的具体实现类,通过反射实现自动实例化依赖项。

当然 LaravelIoC 容器更加复杂,功能更加强大,不过透过上面的小案例,也能感受到一些 IoC 容器的好处。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~