什么是IOC容器(服务容器)

本文深入探讨PHP中IOC容器的实现原理,包括依赖注入、控制反转及依赖反转原则。通过实际案例,讲解如何使用PHP反射机制实现自动依赖注入,以及如何构建一个简单的IOC容器。


原文地址 http://www.piliku.com/2019/11/25/131

本文主要讨论由PHP实现一个IOC容器所使用到的技术。

要了解一个 IOC容器 的实现,总是离不开这些名词:依赖注入(DI = Dependency Injection)、控制反转(IOC = Inversion of Control)、依赖反转原则(DIP = Dependency Inversion Principle),反射。

我们知道,类与类之间的关系(继承、实现、组合、聚合、关联和依赖)当中,其中有一种关系是 依赖关系。

什么是依赖关系?
假设A类的变化引起了B类的变化,则说明B类依赖于A类。
大多数情况下,依赖关系体现在某个类的方法使用另一个类的对象作为参数。
可参考另一篇文章 《类与类之间的关系(UML类图详解)》

http://www.piliku.com/2019/11/20/118

依赖注入

维基百科上是这么说的

在软件工程中,依赖注入是种实现控制反转,用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端)。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务是将会变成客户端的状态的一部分。 传递服务给客户端,而非允许客户端来建立或寻找服务,是本设计模式的基本要求。

简单来说,依赖注入是一种设计模式,实现了控制反转。

以 用户注册成功后把信息存入缓存 的案例来分析下依赖关系和依赖注入。
案例一:

//文件缓存类
class FileCache
{
    public function get($key) {
        echo "取出缓存\n";
    }
    public function put($key, $value) {
        echo "存入缓存\n";
    }
}

//用户注册
class User
{
    private $cache;

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

    //注册逻辑
    public function register($phone)
    {
        //注册成功后,把信息写入缓存
        $this->cache->put('phone',$phone);

        $this->cache->get($phone);
    }
}

//使用
$use = new User(new FileCache());
$use->register('188');

可以看到 User 类中构造函数里依赖 FileCache 类,这就是一个依赖关系。而解决这种依赖关系,是通过在外部传入参数new User(new FileCache())来解决。这就是依赖注入。

所需要的类通过参数的形式传入的就是依赖注入

依赖注入实现方式

上面案例中,User 类的构造函数绑定依赖关系,实例化 User 类时传入依赖类型的对象来实现依赖注入。
依赖注入有下面几种实现方式:

  • (1)基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
  • (2)基于 set 方法。实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
  • (3)基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
  • (4)基于注解。

这里就不一一举例其他几种实现方式。

控制反转

维基里面说,依赖注入实现了控制反转。再看下维基里对控制反转的定义:

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递(注入)给它。

简单来说,控制反转是一种设计原则,主要是用来解耦(解决对象间或系统组件之间的过度依赖问题),而依赖注入则实现了它。是不是可以理解控制反转是指导思想,依赖注入是具体的代码实现?

来看上面的案例好像看不出来哪里有解耦。我们现在增加几种缓存方式来说明依赖注入实现控制反转。
案例二:

//缓存接口
interface Cache
{
    public function get($key);

    public function put($key, $value);
}
//文件缓存类
class FileCache implements Cache
{
    public function get($key) {
        echo "文件缓存:取出缓存\n";
    }

    public function put($key, $value) {
        echo "文件缓存:存入缓存\n";
    }
}
//redis缓存类
class RedisCache implements Cache
{
    public function get($key) {
        echo "redis缓存:取出缓存\n";
    }

    public function put($key, $value) {
        echo "redis缓存:存入缓存\n";
    }
}

//memcache缓存类
class MemCache implements Cache
{
    public function get($key) {
        echo "memcache缓存:取出缓存\n";
    }

    public function put($key, $value) {
        echo "memcache缓存:存入缓存\n";
    }
}

//用户注册
class User
{
    private $cache;

    //参数必须是cache接口的一个实例
    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    //注册逻辑
    public function register($phone)
    {
        //注册成功后,把信息写入缓存
        $this->cache->put('phone',$phone);

        $this->cache->get($phone);
    }
}

//使用
$use = new User(new RedisCache());
$use->register('188');

这样,就是依赖注入实现了控制反转。User 类里的依赖关系,是实现 Cache 接口的一个实例,依赖的是抽象,而不是一个具体的类。

如果还是不理解控制反转,那就想想这里没有依赖注入要怎么写的?比如A类要用到(依赖)B类,那么在A类里面的构造函数直接使用new B(),而不是将类作为参数从外部传入,那这里是不是要把几种缓存方式,在 User 类里全部 new出来,这样就是高度耦合的代码。

这个案例也符合依赖反转原则。

依赖反转原则

依赖反转原则也有叫依赖倒置原则。上面在代码中,User 类里依赖关系,是通过接口来限制的,而不是直接依赖具体类

依赖反转原则规定:

  • (1)高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。

  • (2)抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

上面第二案例中,高层次模块 User 类不再依赖某个具体缓存类,还是依赖缓存 Cache 接口;低层次模块如 FileCache 类也依赖 Cache接口。

到这里,就己经理解了 DI(依赖注入) 和 IOC(控制反转)。

PHP反射机制实现自动依赖注入

在文章最前面己经介绍了依赖注入,通过基于构造函数这种方式实现一个依赖注入的案例。在案例一中,将依赖的资源由外部注入到方法内部(是相对于在方法内部实例化来说的)。就是在调用方法前,将对象(或其他资源)准备好了,再传递给方法,就叫依赖注入。

//使用User类的时候一般这样 手动注入依赖
$use = new User(new FileCache());
$use->register('188x');

像这样每次都要提前把 new FileCache 这样的依赖关系准备好,如果依赖的类很多,就需要重复依赖注入

其实,依赖注入这种模式就己经很强大了,能解决我们日常编程很多问题。无非就是在更复杂的场景下更种模式组合后来简化工作而己。

简单介绍下自动依赖注入,在手动依赖注入的基础上,再也不用从外部转入依赖关系了,它需要借助一个类(方法、容器),通过反射来实现自动解析依赖关系
自动依赖注入通过反射实现。
可以参考另一篇文章 《通过PHP反射实现自动依赖注入(文件上传案例)》

http://www.piliku.com/2019/11/20/118

这里简单讲下反射,主要是因为IOC容器里用到了反射实现自动解析依赖关系。

IOC容器

上面说了这么多就是为了讲IOC容器。
什么是 IOC容器 呢?

IOC容器服务容器(也称有称依赖注入容器、控制反转容器)。

一个功能、命令或者任务都可以叫做服务 service(这里我们一般指提供了一些功能的类)。以上面第二个案例来说,每个类都是一个服务service,如果把所有的 服务(类)装在一个地方,那这个地方叫容器。
容器就是提供服务的载体,在程序运行过程中,动态的提供服务(资源)。

容器本身也是一个服务,本质上只是一个普通的 PHP 类。

实现一个简单的IOC容器

为什么需要服务容器?
大多数时侯,在使用依赖注入方式解耦组件时,并不需要用到容器。当一段程序需要实例化的类太多或者依赖太多的时候,重复依赖注入的代码是比较繁琐的事情。此时需要使用容器。

服务容器是一个用于管理类的依赖和执行依赖注入的强大工具。

一般容器里有两个最重要的方法:绑定和解析。

  • bind(绑定),将要使用的服务对象在容器中注册,它会自动解析绑定的服务所有的依赖关系。相当于将服务装入到了容器中。
  • make(解析),将绑定到容器中的服务从容器中提取出来。

在案例二的基础上,我们添加一个容器:

/**
 * 容器类,将接口与实现绑定
 */
class Container
{
    // 保存与接口绑定的闭包,
    // 闭包必须能够返回接口的实例。
    protected $bindings = [];

    /**
     * 为某个接口绑定一个实现两种方式:
     * 1. 绑定接口的实现的类名
     * 2. 绑定一个闭包,这个闭包应该返回接口的实例(闭包能够在实例化前后进行额外的操作)
     * 两种方式的实例化操作都是调用 build() 方法完成
     * @param $abstract
     * @param null $concrete
     */
    public function bind($abstract, $concrete = null)
    {
        /**
         * 则构建一个闭包
         * 1.如果参数是绑定接口和实现类 如传进来(Cache, FileCache) 则 bindings[Cache] = new FileCache
         *
         * 2.如果参数是绑定依赖和实现 如传进来(User,User) 则 bindings[User] = new User(Cache) 注意:虽然这里看上去传的是接口Cache,但实际上
         * 在第1步的时候注册了一个具体FileCache实现,即 new User(new FileCache)
         */
        if(!$concrete instanceof Closure) {
            // 调用闭包时,传入的参数是容器本身,即 $this
            $concrete = function ($c) use ($concrete) {
                return $c->build($concrete);
            };
        }

        $this->bindings[$abstract] = $concrete;

        //print_r($this->bindings[$abstract]);
    }

    /**
     * 生成指定接口的实例
     * @param $abstract
     * @return mixed
     */
    public function make($abstract)
    {
        // 闭包赋值变量
        $concrete = $this->bindings[$abstract];
        //print_r($concrete($this)->get('dd'));
        //print_r($concrete($this));
        // 运行闭包,即可取得一个实例
        return $concrete($this);
    }

    public function build($concrete)
    {
        // 初始化要反射的具体对象(比如FileCache)
        $reflector = new ReflectionClass($concrete);

        // 检查类是否可实例化
        if (! $reflector->isInstantiable()) {
            // 接口无法实例化
            echo $message = "[$concrete] 无法实例化";
        }

        // 取得构造函数的反射
        $constructor = $reflector->getConstructor();

        // 检查是否有构造函数 (注意,因为我们这里的依赖关系是通过构造函数绑定的)
        //如果依赖关系是通过其他方式(如setter)绑定的,通过反射API如getMethod来拿到依赖关系
        //显然,绑定接口和实现类时,这里就直接实例化实现类(如 FileCache Object ( ))
        if (is_null($constructor)) {
            // 如果没有,就说明没有依赖,直接实例化
            return new $concrete;
        }

        // 取得包含每个参数的反射的数组
        $parameters = $constructor->getParameters();

        // 返回一个真正的参数列表,那些被类型提示的参数已经被注入相应的实例
        $realParameters = [];
        foreach($parameters as $parameter) {
            // 如果一个参数被类型提示为类 Cache,
            // 则这个方法将返回类 Cache 的反射
            $dependency = $parameter->getClass();
            if(is_null($dependency)) {
                $realParameters[] = NULL;
            } else {
                $realParameters[] = $this->make($dependency->name);
            }
        }

        return $reflector->newInstanceArgs($realParameters);
    }
}

客户端使用

$ioc = new Container;
//先绑定接口和某个实现类
$ioc->bind('Cache','FileCache');
//绑定使用类
$ioc->bind('user','User');
$user = $ioc->make("user");
$user->register('iy');

这就是实现IOC容器的核心功能,自动解析依赖注入问题。在实现的过程中,不在需要 new 关键字来实例化对象,更不需要人为的关注组件之间的依赖关系,只需要在服务注册到容器时理清接口与实现类、实现类与依赖接口之间关系,就可以像流水线那样完成实现类的实例化过程。

当程序开始运行的时候,我们把经常复用高的服务,注册到 (bind) 到容器里面,当我需要的时候直接取出来 (make) 就行了。

laravel 框架中的服务容器类实现更为复杂,后面会有一篇文章专门来介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值