一文读懂控制反转(IOC)、依赖注入(DI)、容器(Container)

发布于 2023-08-17 16:48:07

控制反转(IOC)、依赖注入(DI)、容器(Container)

理论定义 (简单读一下就行)

控制反转(Inversion of Control)

当调用者需要被调用者的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例,但在这里,创建被调用者的工作不再由调用者来完成,而是将被调用者的创建移到调用者的外部,从而反转被调用者的创建,消除了调用者对被调用者创建的控制,因此称为控制反转。

依赖注入(Dependency Injection)

要实现控制反转,通常的解决方案是将创建被调用者实例的工作交由IoC容器来完成,然后在调用者中注入被调用者(通过构造器/方法注入实现),这样我们就实现了调用者与被调用者的解耦,该过程被称为依赖注入。依赖注入是控制反转的一种实现方式。常见注入方式有三种:setter、constructor injection、property injection。

容器(Container)

管理对象的生成、资源取得、销毁等生命周期,建立对象与对象之间的依赖关系,可以延时加载对象。比较著名有PHP-DI、Pimple。

示例讲解

这里我们以电脑 USB 接口为例,USB 接口可以插入鼠标键盘U盘 等等一系列的设备。这个时候我们定义一个 USB 类,假设执行的是 Mouse 类鼠标。

/**
 * 定义鼠标类
 */ 
class Mouse
{
    /**
     * 鼠标类执行方法
     */ 
    public function run()
    {
        echo '鼠标正常运行';
    }
}

/**
 * USB 类
 */ 
class USB
{
    /**
     * 接入设备
     */ 
    private $device;
 
    public function __construct()
    {
        $this->device = new Mouse();
    }

    /**
     * USB 设备运行,这里我们运行的是鼠标
     */ 
    public function work()
    {
        $this->device->run();
    }
}

(new USB())->work();
// 这里输出 '鼠标正常运行'
这个时候 USB 类中的 $device 是内部实例化的并且写死了 Mouse, 耦合度太高。这里我们假设有一个 Keyboard 类,USB 中切换到 Keyboard 不使用 Mouse 时,我们就需要在 USB__construct 方法中将 new Mouse() 改成 new Keyboard()。这样没问题,但是如果再改成别的设备,这个类还待需要内部改。这个时候我们可以将设备实例化后当成参数传入到 USB 类时,这样就不需要每次修改内部的 $device 这就是控制反转。

/**
 * 定义鼠标类
 */ 
class Mouse
{
    /**
     * 鼠标类执行方法
     */ 
    public function run()
    {
        echo '鼠标正常运行';
    }
}

/**
 * 定义键盘类
 */ 
class Keyboard
{
    /**
     * 键盘类执行方法
     */ 
    public function run()
    {
        echo '键盘正常运行';
    }
}

/**
 * USB 类
 */ 
class USB
{
    /**
     * 接入设备
     */ 
    private $device;

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

    /**
     * USB 设备运行,这里我们运行的是鼠标
     */ 
    public function work()
    {
        $this->device->run();
    }
}

(new USB(new Mouse()))->work();
// 这里输出 '鼠标正常运行'
(new USB(new Keyboard()))->work();
// 这里输出 '键盘正常运行'
  1. 这里我们 USB 的控制权交给了外部的 new Mouse() new Keyboard() 这种思想就是控制反转,而在 USB__construct($device) 传入的 $device 就是依赖注入,
  2. 注入的方式有三种,分别是:基于构造函数、基于 setter 方法、基于接口。其中我们使用的就是基于构造函数。
一般电脑的 USB 设备都有自己的协议,鼠标、键盘、U盘如果需要正常使用就必须要实现 USB 协议,这里我们就要使用 接口约束,看代码

/**
 * 定义设备接口
 */ 
interface Device
{
    public function run();
}

/**
 * 定义鼠标类
 */ 
class Mouse implements Device
{
    /**
     * 鼠标类执行方法
     */ 
    public function run()
    {
        echo '鼠标正常运行';
    }
}

/**
 * 定义键盘类
 */ 
class Keyboard implements Device
{
    /**
     * 键盘类执行方法
     */ 
    public function run()
    {
        echo '键盘正常运行';
    }
}

/**
 * USB 类
 */ 
class USB
{
    /**
     * 接入设备
     */ 
    private $device;

    public function __construct(Device $device)
    {
        $this->device = $device;
    }

    /**
     * USB 设备运行,这里我们运行的是鼠标
     */ 
    public function work()
    {
        $this->device->run();
    }
}

(new USB(new Mouse()))->work();
// 这里输出 '鼠标正常运行'
(new USB(new Keyboard()))->work();
// 这里输出 '键盘正常运行'

容器的主要功能

  1. 自动管理依赖关系,避免手工管理存在缺陷。
  2. 需要使用依赖时自动注入所需依赖。
  3. 管理对象生命周期。

无论是 Laravel 还是 Thinkphp 5 以后的版本都使用了 Container , 譬如我们经常写类似如下的代码

class IndexController
{
    public function test(Request $request)
    {
        dump($request);
    }
}
我们会发现 Request 类被自动注入切实例化能够使用了,这就是容器 (Container) 帮我们把依赖自动[注入]到类中。

核心的原理其实就是我们通过 反射 来将类中的方法实例化,主要还是用到了反射,这里我就简单写一个[容器]的类

/**
 * 容器类
 */
class Container
{
    /**
     * 容器对象实例
     * @var Container
     */
    protected static $instance;

    /**
     * 容器中实例化对象(laravel中使用singleton)
     * @var array
     */
    protected $instances = [];

    /**
     * 容器中的绑定标识 (laravel 中使用 Bind)
     * @var array
     */
    protected $binds = [];

    /**
     * 初始化容器 (单例)
     */
    public static function getInstance()
    {
        // 如果 Container 没有初始化
        if(is_null(self::$instance)) {
            // 这里使用 new static 而不是new self
            // 当使用 new static使用,当有类库继承 Container 时,实例化的是继承类
            // 使用 new self 实例化的是 Container 本身,所以这里我们使用 new static
            self::$instance = new static;
        }
        // ... 这里不用讲解了吧
        return self::$instance;
    }

    /**
     * 绑定类、闭包,实例等到容器中
     */
    public function bind($abstract, $concrete = null)
    {
        // 当绑定为null时候 $container->bind(Test:class);
        // $abstract = $concrete
        if(is_null($concrete)) {
            $abstract = $concrete;
        }
        // 删除单例绑定
        if(isset($this->instances[$abstract])) {
            unset($this->instances[$abstract]);
        }
        // 如果不是闭包
        if(! $concrete instanceof Closure) {
            // 这里我们创建闭包,让返回都统一是闭包
            $concrete = function($container, $params = []) use ($abstract, $concrete) {
                // 当我们使用 $container->bind(Test:class, Test:class) 时
                // 我们就通过闭包的方式返回实例化的具体实例
                if($abstract == $concrete) {
                    return $container->build($concrete, $params);
                }
                // 当不相等的时候 $container->bind(TestInterface::class, Test::class)
                // 我们就通过容器来解析指定的类型
                return $container->resolve($concrete, $params);
            };
        }elseif(is_object($concrete) && !$concrete instanceof Closure) {
            // 如果传入的是实例化的类
            $this->instances[$abstract] = $concrete;
        }
        // 绑定
        $this->binds[$abstract] = $concrete;
        // 返回容器本身
        return $this;
    }

    /**
     * 通过反射实例化类
     */
    public function build($concrete, $params)
    {
        // 这里我们通过反射的原理来实例化构建
        $reflect = new ReflectionClass($concrete);
        // 先判断是否可以实例化
        if(!$reflect->isInstantiable()) {
            // 这里先直接返回false
            // 正常应该 throw \Exception('错误问题说明等');
            return false;
        }
        // 获取类里边的 __construct() 方法
        $constructor = $reflect->getConstructor();
        // 如果不存在 __construct 方法, 直接返回实例
        if( !$constructor ) {
            return new $concrete;
        }
        // 否则获取 constructor 方法里的参数
        $parameters = $constructor->getParameters();
        // 解析参数
        $args = $this->resolveParameters($parameters, $params);
        // 返回实例
        return $reflect->newInstanceArgs($args);
    }

    /**
     * 通过反射解析参数 注入参数
     */
    public function resolveParameters($parameters, array $params = [])
    {
        $args = [];
        foreach($parameters as $parameter) {
            // 注入参数的类型
            $reflectType = $parameter->getType();
            // 参数名称
            $paramName   = $parameter->getName();
            // 动态参数
            if($parameter->isVariadic()) {
                return array_merge($args, array_values($params));
            } elseif ($reflectType && $reflectType instanceof ReflectionNamedType && !$reflectType->isBuiltin()){
                // 如果注入参数类型是class 那就实例化注入
                $args[] = $this->make($reflectType->getName());
            } elseif ( array_key_exists($paramName, $params)) {
                // 查找数组中是否有指定的参数名称
                $args[] = $params[$paramName];
                // 删除
                unset($params[$paramName]);
            } elseif (!empty($params)) {
                // 如果是数组且没有指定 key 时候从头部按照顺序当做参数使用
                $args[] = array_shift($params);
            } elseif ($parameter->isDefaultValueAvailable()) {
                $args[] = $parameter->getDefaultValue();
            }
        }
        return $args;
    }

    /**
     * 在容器中解析指定的类型
     */
    public function resolve($abstract, $parameters = []) 
    {
        if(isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }
        // 如果存在绑定并且还是闭包的情况下,我们要进行解析
        if(isset($this->binds[$abstract]) && $this->binds[$abstract] instanceof Closure) {
            $function = $this->binds[$abstract];
            $object   = $function($this, $parameters);
        }else{
            // 不存在的情况下我们就通过反射来实例化这个类
            $object   = $this->build($abstract, $parameters);
        }
        // 返回实例
        return $object;
    }

    /**
     * 在容器中解析类的实例
     */
    public function make($abstract, $params = [])
    {
        return $this->resolve($abstract, $params);
    }

    /**
     * 解析容器的实例,并执行对应的方法
     */
    public function makeWithMethod($abstract, $method = '__construct', $params = [])
    {
        $object = $this->resolve($abstract, $params);
        if($method == '__construct'){
            return $object;
        }
        $reflect = new ReflectionMethod($object, $method);
        // 否则获取 constructor 方法里的参数
        $parameters = $reflect->getParameters();
        // 解析参数
        $args = $this->resolveParameters($parameters, $params);
        // 调用类方法
        return $reflect->invokeArgs($object, $args);
    }

    /**
     * 单例绑定模式,在后续调用中返回相同的实例
     */
    public function singleton($abstract, $instance)
    {
        // 如果是闭包,我们将 Container 本身传递给闭包 这样就可以在闭包中使用引用
        if($instance instanceof Closure) {
            $this->instances[$abstract] = $instance($this);
        }else{
            $this->instances[$abstract] = $instance;
        }
    }
}
然后我们开始模拟测试
// 获取容器单例
$container = Container::getInstance();
// 定义一个接口
interface USB {
    public function work();
}
// 定义Mouse
class Mouse implements USB
{
    public function work()
    {
        return 'Mouse is Work';
    }
}
// 这里我们绑定 USB 接口为一个闭包回调,然后返回 USB 类接口
$container->bind(USB::class, function($container, $params){
    return 'USB类接口';
});
//然后解析类 输出 'USB类接口'
echo $container->make(USB::class);
// 我们继续绑定
$container->bind(USB::class, Mouse::class);
// 然后解析类 输出 'USB类接口'
echo $container->make(USB::class)->work();
// 我们经常遇到是如 Laravel 执行控制器中的方法,这个如何操作
/**
 * 模拟定义 Request
 */
class Request
{
    public function all()
    {
        var_dump($_REQUEST);
    }
}

/**
 * 模拟登陆类
 */
class LoginController
{
    public function Login(Request $request, $city)
    {
        $request->all();
        var_dump('城市:'. $city);
    }
}

// 模拟容器执行, 携带参数
$container->makeWithMethod(LoginController::class, 'Login', ['city' => '北京']);
// 当我们浏览器执行 http://localhost/c.php?a=1 的时候
// 就会输出如下
/* array(1) {
    ["a"]=>
    string(1) "1"
  }
  string(6) "北京"
*/
通过上边我们就基本能够清楚 控制反转(IOC)依赖注入(DI)容器(Container) ,如果哪里不清楚或者需要细讲的地方,请留言
0 条评论

发布
问题