
概述
在上一篇中, 讲解了依赖注入(DI), 先留意下面的代码:
// 用宝剑的英雄
new Hero(new Sword('倚天'));
// 用枪的英雄
new Hero(new Gun('沙漠之鹰'));
可见, 在注入时, 需要实例化好所依赖的对象, 再传递到Hero类中.
虽然通过依赖注入解决了解耦的问题, 但在实际使用时, 比较麻烦, 因为每次都需要手动实例化依赖, 再传递. 这对于复杂大量的依赖关系,
手动解决明显力不从心. 因此, 实际项目中需要一个自动化的依赖注入管理机制, 这就是IoC容器.
IoC容器, 一个封装了依赖注入DI的框架, 实现了动态创建注入依赖对象, 管理依赖关系, 管理对象生命周期等功能.
核心实现, 一般分为绑定(注册)对象生成器 和 创建对象注入依赖, 两个核心步骤 我们逐一去看.
绑定(注册)对象生成器
绑定: 指的是将类与生成类对象的代码记录, 对应绑定起来. 这样, 在需要某个类的对象时, 直接执行类对应的生成代码, 就可以得到该类的对象了.
上代码: 同样需要讲解依赖注入是的示例类: Hero, Sword, Gun(看过上篇的直接跳过下面的代码即可)
/**
* 武器接口
*/
interface I_Weapon
{
public function __construct($title);
/**
* 进攻
*/
public function attack();
}
/**
* 宝剑类
*/
class Sword implements I_Weapon
{
private $title;
public function __construct($title)
{
$this->title = $title;
}
public function attack()
{
return '唰唰唰';
}
}
/**
* 枪类
*/
class Gun implements I_Weapon
{
private $title;
public function __construct($title)
{
$this->title = $title;
}
public function attack()
{
return '砰砰砰';
}
}
/**
* 英雄类
*/
class Hero
{
private $weapon;
public function __construct(I_Weapon $weapon)
{
// Hero依赖的武器, 来自于构造时的参数输入,
// 这就将依赖的对象通过注入的方式, 注入到Hero中
$this->weapon = $weapon;
}
}
再看容器类:本节重点
IoC容器示范类
class IoC_Container
{
private static $generator_list = [];
// 绑定类的生成器
public static function bind($class_name, $generator)
{
if (is_callable($generator)) {
self::$generator_list[$class_name] = $generator;
} else {
throw new Exception('对象生成器不是可调结构');
}
}
}
注意其中的bind方法, 就是本小节所说的绑定(注册)对象生成器的实现.
bind方法需要两个参数:
第一个就是类的标志, 通常就是带有命名空间的类名称即可. 例如 path\to\Sword, path\to\Hero.
第二个参数就是该类对应的生成器. 所谓生成器, 其实就是一段代码, 可以生成类对象的代码而已, 说白了就是执行new的代码. 可以是匿名函数, 函数, 类方法等可执行结构都是可以的.
这样其实就是, 要用户提供类 和 该类对象的生成代码, 将其对应, 需要该类对象时执行. 但是注册时, 并不调用生成类的代码, 而仅仅是, 先存储起来. 本示例实现中,就存储到了::$generator_list数组中.
绑定示例代码:
// 利用容器绑定对象生成器
IoC_Container::bind('Sword', function($title='') {
return new Sword($title);
});
IoC_Container::bind('Gun', function($title='') {
return new Gun($title);
});
IoC_Container::bind('Hero', function($module, $params=[]) {
return new Hero(IoC_Container::make($module, $params));
});
调用bind()方法, 提供类名和实例化类对象的匿名函数. 我们的容器就会将类与生成器记录下来, 等着需要时生成.
再看如何创建对象
完善IoC_Container类的代码, 增加创建对象的方法
class IoC_Container
{
private static $generator_list = [];
// 绑定类的生成器
public static function bind($class_name, $generator)
{
if (is_callable($generator)) {
self::$generator_list[$class_name] = $generator;
} else {
throw new Exception('对象生成器不是可调结构');
}
}
// 生成类对象
public static function make($class_name, $params = [])
{
if (! isset(self::$generator_list[$class_name])) {
throw new Exception('类没有被绑定注册');
}
return call_user_func_array(self::$generator_list[$class_name], $params);
}
}
注意其中的make方法, 就是用来生成对象的方法. 该方法要获取需要的类, 然后调用该类之前注册绑定的生成器函数, 来获取对象.
通过make生成类对象:
IoC容器生成对象
$hero1 = IoC_Container::make('Hero', ['Sword', ['倚天']]);
var_dump($hero1);
$hero2 = IoC_Container::make('Hero', ['Gun', ['沙漠之鹰']]);
var_dump($hero2);
这样, 就可以生成两个Hero对象, 一个Sword类对象被注入, 另一个Gun类对象被注入.结果:
object(Hero)#4 (1) {
["weapon":"Hero":private]=>
object(Sword)#5 (1) {
["title":"Sword":private]=>
string(6) "倚天"
}
}
object(Hero)#6 (1) {
["weapon":"Hero":private]=>
object(Gun)#7 (1) {
["title":"Gun":private]=>
string(12) "沙漠之鹰"
}
}
这就是IoC容器的两个基本步骤, 1绑定, 2创建.
中间小结
可见, IoC其实就是一个功能高级点的工厂类而已, 用于生产类的对象. 目前为止, 稍微高级的地方,就是预先将生产类对象的代码, 先绑定了而已. 需要的时候, 在执行生成对象.
而真正解决了依赖问题的, 还是上篇说的DI依赖注入. 而不是这个容器. 容器仅仅是让生成代码更加规范, 方便了而已.
IoC容器, 到此, 基本功能有了, 但是实操时, 往往还需要再升级. 主要升级的方面, 就是自动判断所需要的依赖, 然后生产创建对象时 进行依赖注入. 下面接着聊.
自动依赖注入
上面的例子的make()实现中, 需要手动将Hero需要的参数传递到Make中. 实际的IoC容器, 比这要高级些.
思考我们定义的Hero类的构造方法, 我们使用了类型约束的语法, 限制参数类型:
public function __construct(I_Weapon $weapon)
{
// Hero依赖的武器, 来自于构造时的参数输入,
// 这就将依赖的对象通过注入的方式, 注入到Hero中
$this->weapon = $weapon;
}
那就意味着, 我们是知道Hero类所依赖的对象类型的, 因此我们可以自动根据这个类型, 从已注册的类中, 将Hero需要的Weapon注入(传递)到Hero的构造方法中.
语法上, 我们需要 反射Reflectoin机制, 来了解我们构造方法的参数类型, 去做逻辑判断, 示例代码如下:
为了方便演示自动依赖注入, 将上面的英雄类的依赖改为对 Sword类的依赖, 这样示例代码会直观简单些, 方便学习, 新的英雄Hero类如下:
/**
* 英雄类
*/
class Hero
{
private $weapon;
public function __construct(Sword $weapon)
{
// Hero依赖的武器, 来自于构造时的参数输入,
// 这就将依赖的对象通过注入的方式, 注入到Hero中
$this->weapon = $weapon;
}
}
在IoC容器类中, 增加自动注入的方法:
// 增加自动注入的容器
class IoC_Container
{
private static $generator_list = [];
// 绑定类的生成器
public static function bind($class_name, $generator)
{
// 代码省略
}
// 生成类对象
public static function make($class_name, $params = [])
{
// 代码省略
}
// 自动注入方法
public static function autoInject($class_name)
{
$rf = new ReflectionClass($class_name);
// 获取构造函数
$constructor = $rf->getConstructor();
// 获取构造函数参数
$paramters = $constructor->getParameters();
foreach($paramters as $paramter) {
// 得到参数类型
$type = $paramter->getClass()->getName();
// 生成参数对象
$params[] = self::make($type);
}
return $rf->newInstanceArgs($params);
}
}
上面的代码, 先利用反射类对象, 获取类的构造方法; 再利用方法反射对象,获取构造方法的参数列表; 再利用参数的反射对象, 获取参数的类型, 这样,就可以知道参数要求是Sword类. 那么就可以获Sword类对象, 传递给hero类, 将依赖的Sword类注入到Hero类构造方法中去了.
演示获取英雄类对象:
IoC_Container::bind('Sword', function($title='') {
return new Sword($title);
});
$hero = IoC_Container::autoInject('Hero');
var_dump($hero);
该方法仅仅是一个语法的演示 , 演示如何使用反射完成自动类型判断. 实操中方法的完整性要增加很多操作, 不是上面几行代码就可以完成的.
实际操作中, 会将通用的规范的类, 使用这种IoC容器, 自动依赖注入. 而一些相对特殊规则的类, 可以使用上面的make方法, 解决问题.
那到此, 关于依赖注入的实操中最常见的实现IoC容器, 就说到这里了.
结语
理解了什么是IoC容器, 本文的目的就达到了. 实际使用中(例如laravel)IoC容器的方法会有很多, 例如绑定构造器, 绑定对象实例, 绑定单例, 绑定接口实现等. 具体的使用就要到具体的框架或者产品中看了.
有了IoC容器, 框架的底层, 仅仅提供容器相关的结构, 功能上的模块, 都是容器中注册和生成的.
代码中提到的反射机制语法, 如果大家感兴趣, 可以关注我的PHP基础语法视频系列, 有相关的介绍.
一家之言, 欢迎讨论拍砖.
欢迎关注 小韩说理, 如果还有收获, 帮忙推荐给朋友呢.







