Laravel源码分析之控制反转和依赖注入

概念

  • 控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度, 是一种编程思想,能让我们设计出更为优良的程序。

  • 依赖注入(DI) 是实现IOC的一种方式,通过该方式可以将存在对象内部的依赖转移到外部,动态的将依赖注入到对象内部,实现代码的解耦。

  • 先看下以下代码有什么问题:

    /**
     * 用户类
     */
    class User {
    
        private $name = '张三';
    
        public function name()
        {
            return $this->name;
        }
    }
    
    
    /**
     * 应用类
     */
    class App {
    
        protected $user;
    
        public function __construct()
        {
            $this->user = new User();
        }
    
        public function printUserName()
        {
            echo $this->user->name();
        }
    }
    
    $app = new App();
    $app->printUserName();
    

    代码中定义了一个User类和App类, 在App内部使用new User()创建了一个user实例,如果我想给User类名改成复数Users, 同时还需要去修改App类中内部的代码, 违反了开放封闭原则,这时候如果我们将依赖转移到外部,则可以不用修改内部代码,如下:

    <?php
    
    /**
     * 用户类
     */
    class User {
    
        private $name = '张三';
    
        public function name()
        {
            return $this->name;
        }
    }
    
    
    /**
     * 应用类
     */
    class App {
    
        protected $user;
    
        public function __construct(User $user)
        {
            $this->user = $user;
        }
    
        public function printUserName()
        {
            echo $this->user->name();
        }
    }
    
    $app = new App(new User());
    $app->printUserName();
    

    App类在构造方法中以参数的形式传入,只需要在外部把实例化好的User对象传入。

反射

  • 先来阅读我项目中的UserController控制器的部分代码:

    class UserController extends Controller
    {
        public function __construct(UserService $service)
        {
            $this->service = $service;
        }
    
        /**
         * 用户登录
         * @param LoginRequest $request
         * @return false|string
         */
        public function login(LoginRequest $request)
        {
            return $this->service->login($request->validated());
        }
    
        /**
         * 用户注册
         * @param RegisterRequest $request
         * @return false|string
         * @throws Exception
         */
        public function register(RegisterRequest $request)
        {
            return $this->service->register($request->validated());
        }
    }
    

    可以看到在控制器的构造方法中,注入了一个UserService对象,但是我们根本没有做任何实例化操作,这是Laravel内部帮我们是实现的,那Laravel是如何实现的呢?可能有读者已经想到了,没错,就是反射

  • 反射具有对类、接口、函数、方法和扩展进行反向工程的能力,没使用过的可以参考PHP官方文档-反射

  • 先看一下以下这段反射代码,方便后面理解laravel的ioc。如下:

    <?php
    
    /**
     * 用户类
     */
    class User {
    
        private $name;
    
        public function __construct($name='ClassmateLin')
        {
            $this->name = $name;
        }
    
        public function name()
        {
            return $this->name;
        }
    }
    
    
    // 获取User的reflectionClass对象
    $reflector = new reflectionClass(User::class);
    
    // 拿到User的构造函数
    $constructor = $reflector->getConstructor();
    
    // 拿到User的构造函数的所有依赖参数, 返回的是一个数组
    $dep_params = $constructor->getParameters();
    
    // 创建user对象,不传递参数
    $user = $reflector->newInstance();
    echo $user->name() . PHP_EOL;  // 输出默认值ClassmateLin
    
    $name = $dep_params[0]->name; // 拿到构造函数参数数组的第一个参数的参数名。
    
    // 创建user对象,需要传递参数的
    $user = $reflector->newInstanceArgs($dep_params=[$name => 'XXX']);
    
    echo $user->name() . PHP_EOL;  // 输出XXX
    

    可以看出:
    - 通过reflectionClass传入User类,可以拿到一个反射实例。
    - 通过反射实例可以拿到构造函数。
    - 通过构造函数可以拿到参数列表。
    - 通过反射对象可以创建实例,分为带参数和不带参数两种形式。

简单容器

  • 通过了解上文的内容,我们可以创建一个简单的容器来实现对象的实例化。
  • 比如你的系统用户登录时,依赖一个日志对象, 通过上述内容,你已经可以写出以下代码:
<?php


/**
 * 日志接口
 */
interface Log
{
    public function log($msg);
}

/**
 * 文件日志的实现
 */
class FileLog implements Log
{
    /**
     * 将log进行一个简单的输出
     * @param $msg
     * @return Log|void
     */
    public function log($msg)
    {
        echo '文件日志记录: ' . $msg . PHP_EOL;
    }
}


/**
 * 数据库日志的实现
 */
class DbLog implements Log
{
    public function log($msg)
    {
        echo '数据库日志记录:' . $msg . PHP_EOL;
    }
}


class User
{
    private $log;
    /**
     * @param FileLog $log
     */
    public function __construct(FileLog $log)
    {
        $this->log = $log;
    }

    /**
     * 简单的登录操作
     * @param string $username
     */
    public function login($username='ClassmateLin')
    {
        echo '用户:' . $username . '登录成功!';
        $this->log->log('日志: 用户:' . $username . '登录成功!');
    }
}

该代码中定义了一个日志操作的接口Log, 并且提供了文件方式和数据库方式的日志具体实现。
但是你可能注意到User类中为什么不是注入接口,注入接口Log的化,那不是既可以传入FileLog实例,也可以传入DbLog实例,像这样:

扫描二维码关注公众号,回复: 9331163 查看本文章
class User
{
    private $log;
    /**
     * @param FileLog $log
     */
    public function __construct(Log $log)
    {
        $this->log = $log;
    }

    /**
     * 简单的登录操作
     * @param string $username
     */
    public function login($username='ClassmateLin')
    {
        echo '用户:' . $username . '登录成功!';
        $this->log->log('日志: 用户:' . $username . '登录成功!');
    }
}

$user = new User(new DbLog());
$user = new User(new FileLog());

原因在于反射是不能动态创建接口的,如果想实现的话可以做一个容器绑定,这会在下一篇文章中进行描述。本文先来实现一个简单的容器。

  • 定义一个容器: Application, 提供一个make方法通过传入类名参数,进行实例化:
class Application
{

    function make(string $class_name)
    {

        $reflector = new reflectionClass($class_name); // 拿到反射实例
        $constructor = $reflector->getConstructor(); // 拿到构造函数

        if (is_null($constructor)) { // 如果写构造函数,得到的constructor是null。
            return $reflector->newInstance(); // 进行无参数实例化
        }

        // 拿到构造函数依赖的参数
        $dependencies = $constructor->getParameters();

        // 这时候我们依赖的参数可能也有参数,通过递归的去获取当前类的参数。
        $instance = $this->getDependencies($dependencies);

        // 进行带参数的实例化
        return $reflector->newInstanceArgs($instance);

    }

    /**获取依赖
     * @param $params
     * @return array
     */
    private function getDependencies($params)
    {
        $dependencies = [];
        // array_walk相等于foreach, for 的作用,据说速度是最快的,我也没去验证,只是喜欢闭包。
        array_walk($params, function ($param) use (&$dependencies) {
            $class_name = $param->getClass()->name;  // 获取类名
            $dependencies[] = $this->make($class_name);  // 调用make函数创建实例
        });
        return $dependencies;
    }
}

类中定义了两个函数makegetDependencies,通过递归的形式进行拿构造函数参数进行实例化。递归的出口就是当一个类不需要参数的时候。

  • 创建一个User的实例:
$app = new Application();
$user = $app->make('User'); // 通过传入类名,遍得到了一个user实例
$user->login();

完整代码

如果读完这篇文章有所收获,不妨帮忙点个赞,谢谢老板。

<?php


/**
 * 日志接口
 */
interface Log
{
    public function log($msg);
}

/**
 * 文件日志的实现
 */
class FileLog implements Log
{
    /**
     * 将log进行一个简单的输出
     * @param $msg
     * @return Log|void
     */
    public function log($msg)
    {
        echo '文件日志记录: ' . $msg . PHP_EOL;
    }
}


/**
 * 数据库日志的实现
 */
class DbLog implements Log
{
    public function log($msg)
    {
        echo '数据库日志记录:' . $msg . PHP_EOL;
    }
}


class User
{
    private $log;
    /**
     * @param FileLog $log
     */
    public function __construct(FileLog $log)
    {
        $this->log = $log;
    }

    /**
     * 简单的登录操作
     * @param string $username
     */
    public function login($username='ClassmateLin')
    {
        echo '用户:' . $username . '登录成功!' . PHP_EOL;
        $this->log->log('日志: 用户:' . $username . '登录成功!');
    }
}


class Application
{

    function make(string $class_name)
    {

        $reflector = new reflectionClass($class_name); // 拿到反射实例
        $constructor = $reflector->getConstructor(); // 拿到构造函数

        if (is_null($constructor)) { // 如果写构造函数,得到的constructor是null。
            return $reflector->newInstance(); // 进行无参数实例化
        }

        // 拿到构造函数依赖的参数
        $dependencies = $constructor->getParameters();

        // 这时候我们依赖的参数可能也有参数,通过递归的去获取当前类的参数。
        $instance = $this->getDependencies($dependencies);

        // 进行带参数的实例化
        return $reflector->newInstanceArgs($instance);

    }

    /**获取依赖
     * @param $params
     * @return array
     */
    private function getDependencies($params)
    {
        $dependencies = [];
        // array_walk相等于foreach, for 的作用,据说速度是最快的,我也没去验证,只是喜欢闭包。
        array_walk($params, function ($param) use (&$dependencies) {
            $class_name = $param->getClass()->name;  // 获取类名
            $dependencies[] = $this->make($class_name);  // 调用make函数创建实例
        });
        return $dependencies;
    }
}

$app = new Application();
$user = $app->make('User');
$user->login();
发布了63 篇原创文章 · 获赞 3 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/ClassmateLin/article/details/104441828