- 注册树模式
注册树模式将对象实例注册到一个全局的对象树上,业务层需要的时候可以直接从对象树取出该对象使用,使业务层无需在业务代码中new一个对象,造成两个对象耦合。
使用场景:业务层可以集中从对象容器(即注册树)获取已经创建好的对象。
class RegisterTree{
public static $objects = [];
public static function get($key){
if(!isset(self::$objects[$key])){
self::$objects[$key] = new $key;
}
return self::$objects[$key]
}
public static function set($key, $object){
self::$objects[$key] = $object;
}
public static function unset($key){
unset(self::$objects[$key]);
}
}
set()内部可以不实例化,而仅仅是将类的名字(带命名空间的名字)放入容器中,get取出一个类时才实例化,这样可以节省内存。
如果$this->objects保存的是对象,则外界从注册树取出的实例是一个单例实例;如果$this->objects保存的是类名或一个匿名函数(匿名函数里实现new),get的时候才实例化,那么外界从注册树取出的实例不是一个单例实例,而是互相独立的实例。
- 依赖注入 和 控制反转
依赖注入,简称DI,其作用有两点:减少代码间耦合、分离对象和它所需的外部资源。
依赖注入(DI)和控制反转(IOC,Inversion of controller)表达的是一个意思。
所谓的依赖是指,一个类A作为调用方调用另一个类B,我们称A依赖B。A需要在A自己的方法内部实例化B。
class DbMysql
{
public function query(){}
}
class Controller
{
public $db;
public function __construct()
{
$this->db = new DbMysql();
}
public function action()
{
$this->db->query();
}
}
Controller类在自己的构造方法中实例化了DbMysql类,Controller类和DbMysql类之间的耦合度就比较高,当DbMysql类的构造函数发生改变的时候,比如由现在的没有参数变成有参数了,参数数量改变了,那么Controller类中的代码都要做出相应改变。
或者说我们现在需要将DbMysql类换成另一个DbOracle类,Controller类中要做出的改变甚至更大。
为此我们可以不让调用方实例化一个依赖对象,而是将一个依赖对象作为参数传入调用方,这种模式就是依赖注入。
<?php
class DbMysql
{
public function query(){}
}
class Controller
{
public $db;
public function __construct($dbMysql) // 将$dbMysql注入到构造函数中
{
$this->db = $dbMysql;
}
public function action()
{
$this->db->query();
}
}
$db = new DbMysql();
$c = new Controller($db);
$c->action();
这里负责实例化依赖类 DbMysql和注入 DbMysql实例到 Controller中的工作是在全局环境完成的(你可以理解为是在 index.php 这个环境下注入)。
实际应用中,把实例化和注入这些依赖对象到调用方的工作需要交给专门的类来实现,这种类就是IOC容器(依赖注入容器)。
- 反射
反射可以返回一个类、对象、方法或函数的详细信息(包括对象或类的属性、方法名、方法参数、甚至注释)。
例如:
class person{
public $name;
public $gender;
public function say(){}
public function set($name, $value) {}
public function get($name) {}
}
$student=new person();
// 获取对象属性列表
$reflect = new ReflectionObject($student);
$props= $reflect->getProperties();
var_dump(($props));
// 获取对象方法列表
$m=$reflect->getMethods();
var_dump(($m));
// 获取类的原型
$reflectClass = new ReflectionClass($student);
$props = $reflectClass->getProperties();
$methods=$reflect->getMethods();
var_dump($props);
var_dump($methods);
foreach($props as $p){
var_dump($p->getName());
var_dump($p->isPublic());
var_dump($p->isStatic());
}
// 使用反射执行对象的方法
$student=new person();
$reflectClass = new ReflectionClass($student);
$m = $reflectClass->getMethod("set");
$m->invokeArgs($student, ["age", 25]);
反射可以用于实现动态生成文档,进行调试,在框架中的应用则是用来实现依赖注入。
然而反射的开销较大,能不使用尽量不使用;反射也会破坏类的封装性,因为反射可以使本不应该暴露的方法或属性被强制暴露了出来,这既是优点也是缺点。
- 反射 + 注册树模式 实现 依赖注入容器
依赖注入容器的作用一句话:依赖注入容器负责创建一个调用方对象,以及该调用方依赖的所有对象,并将这些对象注入到调用方中。
反射的作用是创建一个调用方实例时分析注册到IOC容器中的类能否实例化,是否有效,是否有构造函数,构造函数的参数是否限定为特定的类等。
// 依赖注入容器
class Container{
public $instances = [];
// 绑定注册一个依赖实例
public function bind($key, $value){ // $value可以是一个带命名空间的类名或者是一个匿名函数
$this->instances[$key] = $value;
}
// 获取一个依赖实例
public function get($key){
if(!isset($this->instances[$key])){
return $this->build($key);
}
return $this->build($this->instances[$key]);
}
// 将一个闭包或者带命名空间的类名变成实例对象
public function build($value){
if ($value isinstanceof Closure){ // PHP中匿名函数是一个Closure类型的实例
return $value();
}
$reflection = new ReflectionClass($value); // $value是一个类名
// 判断$value能不能实例化,如果$value是一个不存在的类则无法实例
if(!$reflection->isInstantiable()){
return null;
}
// 判断是否有构造函数
$constructor = $reflection->getConstructor()
if(null == $constructor){
return new $value;
}
// 判断构造函数是否有参数
$params = $constructor->getParameters();
if(count($params) == 0){
return new $value;
}
//判断构造函数的参数是否限定了类型
$args = [];
foreach($params as $param){
$c = $param->getClass(); // 如果构造函数的$param形参没有声明是某个类,则$c为null;否则$c是一个对象,$c->name是带有命名空间的类名
$args[] = $c == null ? null : $this->build($c->name);
}
return $reflection->newInstanceArgs($args); // 通过反射实例化 $value这个类
}
}
class DbMysql{
public function __construct($host, $name, $pwd){...}
}
class DbRedis{
public function __construct($host, $name, $pwd){...}
}
// 调用方
class Controller{
public $mysql;
public $redis;
public function __construct(DbMysql $mysql, DbRedis $redis){
$this->mysql = $mysql;
$this->redis = $redis
}
}
// 入口文件
$app = new Container();
// 注册 DbMysql、 DbRedis 和 Controller 类到容器中。假设这三个类和index.php在同一命名空间下
$app->bind("DbMysql", function(){
return new DbMysql('host', 'name', 'pwd');
});
$app->bind("DbRedis", function(){
return new DbRedis('host', 'name', 'pwd');
});
$app->bind('controller', 'controller');
// 从注册树取出controller对象
$controller = $app->get('controller');
instances 保存所有注册的实例,$this->bind 保存所有tp5内置的类;$this->get($name) 可以从容器取出一个对象;$this->set($name, $obj)可以注册一个对象到容器中。
$this->set()如果注册的是一个闭包或者类名,则注册(追加)到 $this->bind中;如果注册的是一个对象,则注册到 $this->instances;
$this->get($name)获取的实例如果在 $this->instances中则直接返回,凡是能从$this->instances取出的对象都是一个单例;否则如果在$this->bind中,则从$this->bind中实例化一个对象并返回;
如果不在$this->bind,则通过反射实例化$name这个类。
如果注册 DbMysql 和 DbRedis 不希望以匿名函数的方式注册,那么也可以将'host', 'name', 'pwd'放在配置文件中,DbMysql在构造函数读取配置文件。
如果希望容器实例化一个类的时候可以传入参数,可以在bind注册的时候传入一些默认参数,在get的时候也可以传入参数。
tp5的容器实现放在 \thinkphp\library\think\Container.php 下。Container容器对象全局只有一个,因此Container本身使用了单例模式。因此tp5的IOC模式使用了 “单例模式 + 注册树模式 + 反射”。
tp5在初始化的时候就会在容器中写死(预注册)一些框架中自身的类。
在我们自身的业务代码中,也可以使用tp5的容器注册一个类或者获取一个类(Container::get("xxx"),便捷方法为app("xxx");注册一个类的便捷方法为 bind("xxx")。这两个便捷方法定义在\thinkphp\helper.php中)。
TP5的依赖注入基本逻辑:
1、tp5的依赖注入容器$this使用单例模式,全局只有1个IOC容器对象;
$this->instances 保存所有注册的实例,$this->bind 保存所有tp5内置的类;
$this->get($name) 可以从容器取出一个对象;$this->set($name, $obj)可以注册一个对象到容器中。
2、$this->set()如果注册的是一个闭包或者类名,则注册(追加)到 $this->bind中;如果注册的是一个对象,则注册到 $this->instances;
$this->get($name)获取的实例如果在 $this->instances中则直接返回,凡是能从$this->instances取出的对象都是一个单例;否则如果在$this->bind中,则从$this->bind中实例化一个对象并返回;如果不在$this->bind,则通过反射实例化$name这个类。