依赖反转原则
控制反转(IoC)
控制反转 (Inversion of control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式实现方式是依赖注入(Dependency Injection,简称DI),还有一种方式叫依赖查找(Dependency Lookup,简称DL)
控制反转简单来说就是依赖对象的获得被反转了,因为大多数应用程序都是由两个或是更多的类通过彼此的合作来实现业务逻辑,这使得每个对象都需要获取与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。
IoC 把传统模式中需要自己通过 new 实例化,或者通过工厂模式实例化的任务交给第三方,借助于“第三方”实现具有依赖关系的对象之间的解耦。通俗的来理解,就是本来当需要某个类的某个方法时,本来需要自己主动实例化,而现在不再需要考虑如何实例化其依赖的类。
依赖注入(DI)
依赖注入是一种具体的编程技巧,是实现 IoC 的一种方法。不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
下面我们通过一个具体的例子来看看,就拿我们最常用的通知模块举例:
<?php
// 定义了一个消息类
class Message
{
public function seed()
{
return 'seed email';
}
}
// 订单产生的时候 需要发送消息
class Order
{
protected $messager = '';
function __construct()
{
$this->messager = new Message();
}
public function send_msg()
{
return $this->messager->seed();
}
}
$Order = new Order();
$Order->seed_msg();
上面的代码是我们传统的写法。首先定义一个邮件消息发送的类。然后在我们需要发送消息的地方,调用发送消息的接口。
这样做,消息发送就被硬编码进了整个订单逻辑,相互耦合。
有一天你需要添加一个发送短信的接口以满足不同的需求。那么你会发现你要在 Message 类里面做修改。同样也要在 Order 类里面做修改。这样就显得很麻烦。下面我们把代码做一个调整:
/**
* 为了约束我们先定义一个消息接口
* Interface Message
*/
interface Message
{
public function send();
}
/**
* 有一个发送邮件的类
* Class SeedEmail
*/
class SeedEmail implements Message
{
public function send()
{
// TODO: Implement send() method.
}
}
/**
*新增一个发送短信的类
* Class SeedSMS
*/
class SeedSMS implements Message
{
public function send()
{
// TODO: Implement send() method.
}
}
/*
* 订单产生的时候 需要发送消息
*/
class Order
{
protected $messager = '';
function __construct(Message $message)
{
$this->messager = $message;
}
public function send_msg()
{
return $this->messager->send();
}
}
// 我们需要发送邮件的时候
$message = new SeedEmail();
// 将邮件发送对象作为参数传递给Order
$Order = new Order($message);
$Order->send_msg();
// 我们需要发送短信的时候
$message = new SeedSMS();
$Order = new Order($message);
$Order->send_msg();
这样,我们的订单在实现的时候不需要知道其依赖的类的具体信息,只需要知道这个类实现了它需要的接口,而对接口的细节可以完全忽略。
不过虽然上面的代码可以进行依赖注入了,但是依赖还是需要手动创建。我们可不可以创建一个工厂类,用来帮我们进行自动依赖注入呢?答案是可以的,我们需要一个 IoC 容器。
IoC容器
依赖注入是以构造函数参数的形式传入的,想要自动注入:
- 我们需要知道需求方需要哪些依赖,使用反射来获得
- 只有类的实例会被注入,其它参数不受影响
如何自动进行注入呢?当然是 PHP 自带的反射功能!
注:关于反射是否影响性能,答案是肯定的。
但是相比数据库连接、网络请求的时延,反射带来的性能问题在绝大多数情况下并不会成为应用的性能瓶颈。
目前主流的PHP框架也基本都引入了IoC容器:Laravel、Yii、
TP6
简单的容器
首先,创建 Container
类,实现 getInstance
方法用于自动依赖注入
class Container
{
public static function getInstance($class_name, $params = [])
{
// 获取反射实例
$reflector = new ReflectionClass($class_name);
// 获取反射实例的构造方法
$constructor = $reflector->getConstructor();
// 获取反射实例构造方法的形参
$di_params = [];
if ($constructor) {
foreach ($constructor->getParameters() as $param) {
$class = $param->getClass();
// 如果参数是一个类,创建实例,并对实例进行依赖注入
if ($class) {
$di_params[] = self::getInstance($class->name);
}
}
}
$di_params = array_merge($di_params, $params);
// 创建实例
return $reflector->newInstanceArgs($di_params);
}
}
这里我们获取构造方法参数时用到了 ReflectionClass 类,大家可以到官方文档了解一下该类包含的方法和用法,这里就不再赘述。
使用递归完成了多层依赖的注入关系,程序中依赖关系层级一般不会特别深,递归不会造成内存遗漏问题。
ok,有了 getInstance
方法,我们可以试一下自动注入依赖了。为了简单展示多层依赖注入,我们使用另外的三个类:A、B、C:
class A
{
public $count = 20;
}
class B
{
public $count = 100;
public function __construct(A $a)
{
$this->count += $a->count;
}
}
class C
{
protected $count = 1;
public function __construct(B $b, $count)
{
$this->count = $b->count + $count;
}
public function getCount()
{
return $this->count;
}
}
$c = Container::getInstance(C::class, [10]);
var_dump($c->getCount()); // result is 130
支持单例模式
有些类会贯穿在程序生命周期中被频繁使用,为了在依赖注入中避免不停的产生新的实例,我们需要 IoC 容器支持单例模式,已经是单例的依赖可以直接获取,节省资源
class Container
{
protected static $_singleton = [];
// 添加一个实例到单例
public static function singleton($instance)
{
if (!is_object($instance)) {
throw new InvalidArgumentException("Object need!");
}
$class_name = get_class($instance);
// singleton not exist, create
if (!array_key_exists($class_name, self::$_singleton)) {
self::$_singleton[$class_name] = $instance;
}
}
// 获取一个单例实例
public static function getSingleton($class_name)
{
return array_key_exists($class_name, self::$_singleton) ?
self::$_singleton[$class_name] : null;
}
// 销毁一个单例实例
public static function unsetSingleton($class_name)
{
self::$_singleton[$class_name] = null;
}
public static function getInstance($class_name, $params = [])
{
// 获取反射实例
$reflector = new ReflectionClass($class_name);
// 获取反射实例的构造方法
$constructor = $reflector->getConstructor();
// 获取反射实例构造方法的形参
$di_params = [];
if ($constructor) {
foreach ($constructor->getParameters() as $param) {
$class = $param->getClass();
if ($class) {
// 如果依赖是单例,则直接获取
$singleton = self::getSingleton($class->name);
$di_params[] = $singleton ?? self::getInstance($class->name);
}
}
}
$di_params = array_merge($di_params, $params);
// 创建实例
return $reflector->newInstanceArgs($di_params);
}
}
以依赖注入的方式运行方法
类之间的依赖注入解决了,我们还需要一个以依赖注入的方式运行方法的功能,可以注入任意方法的依赖。这个功能在实现路由分发到控制器方法时很有用。
需要给 Container
类增加一个 run
方法
public static function run($class_name, $method, $params = [], $construct_params = [])
{
if (!class_exists($class_name)) {
throw new BadMethodCallException("Class $class_name is not found!");
}
if (!method_exists($class_name, $method)) {
throw new BadMethodCallException("undefined method $method in $class_name !");
}
// 获取实例
$instance = self::getInstance($class_name, $construct_params);
// 获取反射实例
$reflector = new ReflectionClass($class_name);
// 获取方法
$reflectorMethod = $reflector->getMethod($method);
// 查找方法的参数
$di_params = [];
foreach ($reflectorMethod->getParameters() as $param) {
$class = $param->getClass();
if ($class) {
$singleton = self::getSingleton($class->name);
$di_params[] = $singleton ? $singleton : self::getInstance($class->name);
}
}
// 运行方法
return call_user_func_array([$instance, $method], array_merge($di_params, $params));
}
// $result = Container::run(C::class, 'getCount', [],[2]);
// var_dump($result); // result is 122