控制反转与依赖注入 (2) - 实现一个简单 IoC 容器来理解依赖自动注入的好处
手握设计模式宝典 - 控制反转与依赖注入 (2) - 实现一个简单 IoC 容器来理解依赖自动注入的好处
Design-Pattern - Inversion-Of-Control and Dependency-Injection
- Title: 《手握设计模式宝典》 之 控制反转与依赖注入 (2) - 实现一个简单 IoC 容器来理解依赖自动注入的好处
- Tag:
Design-Pattern
、Inversion-Of-Control
、Dependency-Injection
、IoC
、DI
、设计模式
- Author: Tacks
- Create-Date: 2023-10-17
- Update-Date: 2023-10-17
大纲
- 手握设计模式宝典 - 控制反转与依赖注入 (2) - 实现一个简单 IoC 容器来理解依赖自动注入的好处
0、Series
- 控制反转与依赖注入 (1) - 先区分好 IoC 和 DI 是什么
- 控制反转与依赖注入 (2) - 实现一个简单 IoC 容器来理解依赖自动注入的好处
- 控制反转与依赖注入 (3) - 构建最小容器实现PSR-11规范
0、REF
- @Fabien Potencier | What is Dependency Injection?
- @liuqing_hu | 什么是依赖注入
- @Epona | [译] 依赖注入?? 哈??
- @skyLee | Laravel 依赖注入原理
- @snail | 浅析依赖倒转、控制反转、IoC 容器、依赖注入
- @PHP | 反射 - ReflectionMethod
- @PHP | 反射 - ReflectionNamedType
- @PHP | 反射 - ReflectionClass
- @PHP | 回调 - call_user_func_array(callable $callback, array $args): mixed
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、 概念
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)
?
这并没有给我们带来灵活的注入,反而增加了成本,甚至不如简单工厂模式的方式,根据不同类型来进行咖啡机的实例化。
而且,这也并没有解决依赖的关系,依然是 LatteCoffee
和 UserAutoA
依赖在一起。
这里可以了解一下 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]
,表示当解析 VideoController
或 UploadController
类时,应用下面的上下文绑定规则。依赖项的抽象目标是 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
- 自动实例化依赖项
- IoC
make()
方法,利用反射机制Reflection
,获取依赖类的构造函数参数,并根据绑定关系实例化对应的依赖对象。
- IoC
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 容器,就是一个容器类,里面保存各种我们需要的对象,并且实现依赖注入,帮助我们解决类之间的依赖问题。通过绑定确定接口依赖项的具体实现类,通过反射实现自动实例化依赖项。
当然 Laravel
中 IoC
容器更加复杂,功能更加强大,不过透过上面的小案例,也能感受到一些 IoC
容器的好处。