我们先来介绍控制反转,依赖注入,这两个概念我们可以认为他们表达的同一种意思,举个通俗的例子,我们用户登录需要提供记录日志的功能,可以选择使用文件或者数据库。下面我们用代码来演示。
<?php
/**
* Interface Log
* 接口规范
*/
interface Log
{
/**
* @param string $content
* @return mixed
*/
public function write($content = '');
}
/**
* Class LogFile
* 文件log
*/
class LogFile implements Log
{
/**
* @param string $content
* @return mixed
*/
public function write($content = '')
{
echo 'file log.' . PHP_EOL;
}
}
/**
* Class LogDB
* 数据库log
*/
class LogDB implements Log
{
/**
* @param string $content
* @return mixed
*/
public function write($content = '')
{
echo 'db log.' . PHP_EOL;
}
}
/**
* Class User
* 程序操作
*/
class User
{
/** @var null $log */
public $log = null;
/**
* User constructor.
*/
public function __construct()
{
$this->log = new LogFile();
}
public function login()
{
echo 'user login!' . PHP_EOL;
$this->log->write('User Login succ!');
}
}
class UserIoc
{
/** @var null $log */
public $log = null;
/**
* User constructor.
*/
public function __construct(LogDB $log)
{
$this->log = $log;
}
public function login()
{
echo 'user login!' . PHP_EOL;
$this->log->write('User Login succ!');
}
}
// 普通调用
$user = new User();
$user->login();
// Ioc 控制反转
$userIoc = new UserIoc(new LogDB());
$userIoc->login();
上面实现的普通调用记录日志的功能,但是有一个问题,假如你想在想要使用数据库记录日志的话,就需要需改User
类,这部分代码没达到解耦合,也不符合编程开放封闭原则。 所以,我们可以把日志处理类通过构造函数方式传递进去。下面我们试着修改 UserIoc
类的代码。
这样想用任何方式记录操作日志都不需要去修改 User 类了,只需要通过构造函数参数传递就可以实现,其实这就是 “控制反转”。不需要自己内容修改,改成由外部传递。这种 由外部负责其依赖需求的行为,我们可以称其为 “控制反转(IoC)”。
那什么是依赖注入呢?,其实上面的例子也算是依赖注入,不是由自己内部 new 对象或者实例,通过构造函数,或者方法传入的都属于 依赖注入(DI)。
初学 laravel 的同学应该都比较好奇?很多对象实例通过方法参数定义就能传递进来,调用的时候不需要我们自己去手动传入。下面举一个 laravel 中实际的例子 Request 对象 会都被自动的注入到函数里。是不是比较好奇呢?laravel 是如何做到呢?
// routes/web.php
Route::get('/post/store', 'PostController@store');
// App\Http\Controllers
class PostController extends Controller {
public function store(Illuminate\Http\Request $request)
{
$this->validate($request, [
'category_id' => 'required',
'title' => 'required|max:255|min:4',
'body' => 'required|min:6',
]);
}
}
我们现在已经明白了依赖注入的概念。那 laravel 那种用法怎么实现呢?可能有些同学已经想到了这里面肯定会用到反射机制去创建动态 Post,然后去调用 store 方法。
反射的概念其实可以理解成根据类名返回该类的任何信息,比如该类有什么方法,参数,变量等等。我们先来学习下反射要用到的 api。拿 User 举例
// 获取User的reflectionClass对象
$reflector = new reflectionClass(User::class);
// 拿到User的构造函数
$constructor = $reflector->getConstructor();
// 拿到User的构造函数的所有依赖参数
$dependencies = $constructor->getParameters();
// 创建user对象
$user = $reflector->newInstance();
// 创建user对象,需要传递参数的
$user = $reflector->newInstanceArgs($dependencies = []);
这时候我们可以创建一个 make 方法,传入 User,利用反射机制拿到 User 的构造函数,进而得到构造函数的参数对象。用递归的方式创建参数依赖。最后调用 newInstanceArgs 方法生成 User 实例。 可能有些同学还不是很理解。下面我们用代码去简单模拟下
function make($concrete){
// 或者User的反射类
$reflector = new ReflectionClass($concrete);
// User构造函数
$constructor = $reflector->getConstructor();
// User构造函数参数
$dependencies = $constructor->getParameters();
// 最后生成User
return $reflector->newInstanceArgs($dependencies);
}
$user = make('User');
$user->login();
// 注意我们这里需要修改一下User的构造函数,如果不去修改。反射是不能动态创建接口的,那如果非要用接口该怎么处理呢?下一节我们讲Ioc容器的时候会去解决。
class User
{
protected $log;
public function __construct(FileLog $log)
{
$this->log = $log;
}
public function login()
{
// 登录成功,记录登录日志
echo 'login success...';
$this->log->write();
}
}
function make($concrete){
$reflector = new ReflectionClass($concrete);
$constructor = $reflector->getConstructor();
// 为什么这样写的? 主要是递归。比如创建FileLog不需要传入参数。
if(is_null($constructor)) {
return $reflector->newInstance();
}else {
// 构造函数依赖的参数
$dependencies = $constructor->getParameters();
// 根据参数返回实例,如FileLog
$instances = getDependencies($dependencies);
return $reflector->newInstanceArgs($instances);
}
}
function getDependencies($paramters) {
$dependencies = [];
foreach ($paramters as $paramter) {
$dependencies[] = make($paramter->getClass()->name);
}
return $dependencies;
}
$user = make('User');
$user->login();
到这里,我们依赖注入,控制翻转,反射也就讲完了。 但其实代码还是没有完全达到解偶,以后有机会再继续介绍吧。