PHP高级特性-反射以及工厂设计模式的结合使用 [结合 Laravel-Admin 代码实例讲解]
利用反射来实现工厂模式的生产而无需创建特定的工厂类
本文地址http://janrs.com/?p=833转载无需经过作者本人授权
转载请注明来源
反射[Relfection]
什么是Reflection
Reflection
,即反射。反射提供给面向对象编程可以自省的能力
这么理解有点太过于概念化,通俗地讲,就是能根据事件的结果反查出原因。在编程中,可以根据一个被实例化的对象,反查出这个对象属于的类以及该类拥有所有属性以及方法,甚至可以读取文档注释。这个反查的过程就叫做反射
PHP
提供了完整的反射 API
,提供了内省类、接口、函数、方法和扩展的能力。此外,反射 API
提供了方法来取出函数、类和方法中的文档注释。详细见PHP官网
PHP反射简介
Reflection
能干什么
在上面讲到的,可以使用反射来获取一个类的所有属性以及方法还有注释文档,甚至可以获取类属性和方法的访问权限[protected/private]
,这些特性使得PHP的使用灵活性得到非常大的提高。例如:
- Laravel
框架的所谓优雅所在,即容器、依赖注入、IOC
控制反转就是依靠这些特性实现的
- Hyperf
框架的注解路由也是根据反射获得注释来实现的
- 生成文档
因为反射可以获取类属性和方法的访问权限,可以扫描整个项目的所有文件再使用反射来生成文档
- 测试驱动开发
利用反射获取该类的所有方法的特性,进行测试驱动开发
- 开发插件
利用反射获取类的内部结构的特性,实现 Hook
功能,例如框架插件的实现
Reflection
的优缺点
优点
反射提供了对类的反解析,从而相比原本面向对象的编程方式获得了极高的灵活性,以及合理的使用能够让代码看起来更加优雅以及简洁。原本在面向对象的编程方式中,使用一个类的实例需要先 new
出一个对象再使用方法,但是使用了反射机制,只需要提供一个该类的方法然后使用反射机制即可使用该对象或者方法。Laravel
框架正是使用了大量的反射才获得了优雅的美誉,Swoole
的 Hyperf
框架的注解路由的实现也是使用了反射
缺点
同时,由于反射是类实例化的反过程,破坏了面向对象的封装性,直接将类的整个内部结构暴露,这就导致了反射一旦滥用,代码将难于管理,整个项目将非常混乱,甚至导致业务执行错乱。尤其在大项目几十人的团队中,试想一下,原本的面向对象,只告诉什么可以用,什么不可以用,CTO写好了底层代码,其他人继承后然后使用就行,内部结构啥的其他人都不知道。一旦用上了反射,如果有一个程序员不小心将原本是 protected
或者是 private
的属性或者方法设置成了可以访问,其他程序员在不知情的情况调用了本该隐藏的数据或者方法,那将导致不可预测的灾难【见下面示例代码】
其次,由于反射的灵活性极高,这导致了无法在 IDE
中通过直接直接点击代码溯源,对于新手真的是很蛋疼,Laravel
和Hyperf
都是如此
在下面的代码中,反射的机制直接将 private
方法设置成外部可访问
#Example:
<?php
class Foo {
private function myPrivateMethod() {
return 7;
}
}
$method = new ReflectionMethod('Foo', 'myPrivateMethod');
//该反射功能直接将原本是private权限的方法设置成可访问
$method->setAccessible(true);
echo $method->invoke(new Foo);
// echos "7"
?>
工厂设计模式
三种工厂设计模式 [简单工厂模式] [工厂模式] [抽象工厂模式]
简单工厂模式
又称为静态工厂方法模式。简单的说,就是创建对象的方式是通过一个静态方法来实现的。在简单工厂模式中,根据传递的参数来返回不同的类的实例
在PHP
中在简单工厂模式中,有一个抽象的产品类【即abstract class Calculate
】,这个抽象类可以是接口/抽象类/普通类
。这个抽象的产品类可以派生出多个具体的产品类【即class CalculateAdd
以及class CalculateSub
】。最后再由一个具体的工厂类【即class CalculateFactory
】来获取所需要的产品类的实例
![JARNS.COM - 工厂模式[简单工厂UML图]]
代码实现
1) 抽象产品生产类:运算抽象类
//生产抽象类
abstract class Calculate{
//数字A
protected $number_a = null;
//数字B
protected $number_b = null;
//设置数字A
public function setNumberA( $number ){
$this->number_a = $number;
}
//设置数字B
public function setNumberB( $number ){
$this->number_b = $number;
}
//获取数字A
public function getNumberA(){
return $this->number_a;
}
//获取数字B
public function getNumberB(){
return $this->number_b;
}
//获取计算结果【获取生产出的产品】
public function getResult(){
return null;
}
}
2) 具体产品生产类:加法运算 / 减法运算 等等
//加法运算
class CalculateAdd extends Calculate{
//获取运算结果【获取具体的产品】
public function getResult(){
return $this->number_a + $this->number_b;
}
}
//减法运算
class CalculateSub extends Calculate{
//获取运算结果【获取具体的产品】
public function getResult(){
return $this->number_a - $this->number_b;
}
}
//乘法 / 除法 等等其他运算【其他产品】
3) 工厂:工厂类。即用一个单独的类来创造实例化的过程,这个类就是工厂。也就是 简单工厂模式
在 php
中,实现的方式其实就一个 switch
函数或者是 php8
新出的 match
函数来实例化所需要的产品生产类
//根据运算不同实例化不同的对象
//【也就是根据所需产品,实例化对应的产品类进行生产】
//对应的实现其实就是一个switch或者php8函数新出的match函数
//下面用最新的match函数做演示
class CalculateFactory{
public static function setCalculate( $type = null ){
return match( $type ){
'add' => (function(){
return new CalculateAdd();
})(),
'sub' => (function(){
return new CalculateSub();
})(),
default => null;
};
}
}
//具体使用
$calculate = CalculateFactory::setCalculate('add');
$calculate->setNumberA = 1;
$calculate->setNumberB = 2;
//计算
echo $calculate->getResult;//echo 3
总结
:简单工厂模式其实就是创建一个基类【abstract
】,该类存放所有具体生产产品类的共用的代码
,但是没有执行过程
,然后具体生产产品的类全部继承基类再实现各自的生产过程
。最后创建一个工厂类,该类用来根据传入的参数
来获取所需的生产类
工厂方法模式
又称为工厂模式,属于创造型模式。在工厂模式中,工厂类的父类只负责定义公共接口,并不执行实际的生产动作。实际的生产动作则交给工厂的子类来完成。这样做将类的的实例化延迟到了工厂的子类,通过工厂的子类来完成实例化具体的产品,也就是生产
在工厂模式中,跟简单工厂模式不一样的是,有一个抽象的工厂类【即interface CalculateFactory
】,可以是接口/抽象类
,这个抽象的工厂类可以派生出多个具体的工厂类【即FactoryAdd
以及FactorySub
】
![JARNS.COM - 工厂模式[工厂UML图]]
代码实现【以下代码需要用到上面的生产抽象类】
以下代码需要用到上面的生产抽象类:abstract class Calculate
以及具体的生产类,即:CalculateAdd
以及 CalculateSub
。下面不再重复实现
interface CalculateFactory{
public function CreateCalculate();
}
class FactoryAdd implements CalculateFactory{
public function CreateCalculate(){
return new CalculateAdd();
}
}
class FactorySub implements CalculateFactory{
public function CreateCalculate(){
return new CalculateSub();
}
}
//具体使用
//创建工厂实例
$calculateFactory = new FactoryAdd();
$add = $calculateFactory->CreateCalculate();
$add->setNumberA( 1 );
$add->setNumberB( 2 );
//计算
echo $add->getResult();//echo 3
总结:工厂模式相比于简单工厂模式的区别在于,在简单工厂模式中,只有一个工厂
来生产对应的生产对象【即CalculateFactory
】。而在工厂模式中,每一个生产产对象都由自己的工厂
来生产,并且这些工厂都继承
自同一个接口【即 interface CalculateFactory
】
抽象工厂模式
抽象工厂模式提供创建一系列相关或相互依赖对象的接口,而且无需指定它们具体的类。这么理解很抽象。通俗一点的解释就是,相比于上面的工厂模式来讲,抽象工厂模式在每个不同的工厂之上又有一个超级工厂,这个超级工厂是抽象的接口【interface
】,用来生产具体的工厂
在抽象工厂模式中,有多个抽象的产品类【即abstract class Phone
以及abstract class Android
】,可以是接口/抽象类/普通类
,每个抽象产品类可以派生出多个具体产品类【即class IPhone
/ class MiPhone
以及 class IOS
/ class Android
】。一个抽象的工厂类【即interface AbstractFactory
】可以派生出多个具体的工厂类【即class iPhoneFactory
以及class MiFactory
】,且每个具体的工厂类可以创建多个产品类的实例【即都有createPhone
和createSystem
】
![JARNS.COM - 抽象工厂模式[工厂UML图]]
代码实现
//抽象的产品类
abstract class Phone{}
abstract class System{}
//具体的产品类
class IPhone extends Phone{}
class MiPhone extends Phone{}
//具体的产品类
class IOS extends System{}
class Android extends System{}
//超级工厂
interface AbstractFactory{
public function createPhone();
public function createSystem();
}
//具体的苹果工厂
class iPhoneFactory implements AbstractFactory{
//生产苹果手机
public function createPhone(){
return new IPhone();
}
//生产苹果系统
public function createSystem(){
return new IOS();
}
}
//具体的小米工厂
class MiFactory implements AbstractFactory{
//生产小米手机
public function createPhone(){
return new MiPhone();
}
//生产安卓系统
public function createSystem(){
return new Android();
}
}
总结:抽象工厂模式相比于工厂模式,抽象工厂模式提供了一个接口用来规定所需要生产的产品。每个继承于该接口的工厂都能按照指定的模式进行生产【代码中的AbstarctFactory
】
以上三种工厂模式,最终都是为了将重复的代码提取出来,并且按照特定的需求场景归纳好,进行解耦和复用,以便在需要的场景中直接使用
三种模式的概括为:
简单工厂:
一个抽象产品类(可以是:接口,抽象类,普通类),可以派生出多个具体产品类
单独一个具体的工厂类
每个具体工厂类只能创建一个具体产品类的实例
工厂模式:
一个抽象产品类(可以是:接口,抽象类,普通类),可以派生出多个具体产品类
一个抽象工厂类(可以是:接口,抽象类),可以派生出多个具体工厂类
每个具体工厂类只能创建一个具体产品类的实例
抽象工厂:
多个抽象产品类(可以是:接口,抽象类,普通类),每个抽象产品类可以派生出多个具体产品类
一个抽象工厂类(可以是:接口,抽象类),可以派生出多个具体工厂类
每个具体工厂类可以创建多个具体产品类的实例
三个模式之间的区别:
简单工厂模式只有一个抽象产品类,只有一个具体的工厂类
工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个抽象产品类
工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个具体产品类的实例
工厂模式与反射的结合使用
可以利用反射的特性来实现工厂模式的生产过程,结合Laravel-admin
进行举例
先看下以下的代码,需求背景:需要根据角色不同显示不同的权限按钮
<?php
class TaskController extends BaseController
{
use HasResourceActions;
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
//Grid Columns...
if (Admin::user()->inRoles([AdminUserModel::getAssignmentRole()])) {
$grid->disableBatchActions();
$grid->disableEditButton();
$grid->disableCreateButton();
$grid->disableDeleteButton();
} elseif (Admin::user()->inRoles([AdminUserModel::getEvaluatorRole()])) {
$grid->disableBatchActions();
$grid->disableEditButton();
$grid->disableCreateButton();
$grid->disableDeleteButton();
$grid->actions(function (Grid\Displayers\Actions $actions) {
$actions->append(new ConfirmCloseTaskAction());
});
} else {
$grid->disableCreateButton();
$grid->disableDeleteButton();
$grid->disableEditButton();
$grid->disableBatchActions();
$grid->disableViewButton();
$grid->disableActions();
}
}
}
以上的代码很明显一看就显得很臃肿。且随着业务的增加【即Controller
的增加】以及角色的增加,需要写更多重复的判断以及重复的代码
解决思路:不同的角色需要拥有的不同的权限,每个角色都可以用一个固定的方法来设置权限,这个固定的方法可以为不同的角色设置权限。这些条件刚好满足工厂模式的使用场景:即:
抽象出一个产品类来派生出多个角色的权限产品类
抽象出一个工厂类来派生出多个具体的工厂类,这些工厂类表现为对应要使用权限按钮的场景
每个具体工厂【使用权限按钮的场景】可以创建多个具体产品类【即实例化多个角色的权限产品】
代码如下【在下面的代码中,将使用反射来代替工厂的生产】
1) 抽象出一个产品类来派生出多个角色的权限产品类
<?php
namespace App\GridActionFactory;
use Dcat\Admin\Grid;
/**
* 工厂接口
*/
interface GridActionInterface
{
//业务员角色的权限
function salesmanAction(Grid $grid);
//分配员角色的权限
function assignmentAction(Grid $grid);
//财务角色的权限
function financeAction(Grid $grid);
//....其他角色的权限
}
2,3) 2,3两个步骤包含在一起
。抽象出一个工厂类来派生出多个具体的工厂类,这些工厂类表现为对应要使用权限按钮的场景。其中,setRoleAction
方法使用反射来直接生产,也就是替代了每个具体工厂类创建实例的过程
<?php
namespace App\GridActionFactory;
use Dcat\Admin\Admin;
use Dcat\Admin\Grid;
/**
* 设置Action权限抽象类
*/
abstract class GridActionAbstract
{
//
abstract public static function setAction(Grid $grid, string $role);
/**
* 过滤角色
*
* @param string $role
* @return bool
*/
protected static function isInRoles(string $role): bool
{
return Admin::user()->inRoles([$role]);
}
/**
* 调用对应的方法
* [该方法其实就是工厂模式中的工厂,专门来生产的]
* [多个工厂对应的就是各个需要用到Action权限的Controller控制器]
* [每个Controller控制器来生产自己的Action权限]
* [这个生产是通过反射来实现]
*
* @param Grid $grid
* @param string $role
* @param string $class
* @throws \ReflectionException
*/
protected static function setRoleAction(Grid $grid, string $role, string $class)
{
$r = new \ReflectionClass($class);
$methodName = $role . 'Action';
if (!$r->hasMethod($methodName))
throw new \Exception('Method Not Found [ method : ' . $methodName . ' ] ');
$method = $r->getMethod($methodName);
$method->invoke($r->newInstance(), $grid);
}
}
根据以上的反射来实现实例化的过程,上面的TaskController
的权限可以简化成下面的代码:
<?php
namespace App\GridActionFactory;
use Dcat\Admin\Grid;
class TaskAction extends GridActionAbstract implements GridActionInterface
{
/**
* @param Grid $grid
* @param string $role
* @throws \ReflectionException
*/
public static function setAction(Grid $grid, string $role)
{
if (!parent::isInRoles($role)) return;
//通过调用父类的setRoleAction直接实现生产的过程
parent::setRoleAction($grid, $role, self::class);
}
//在TaskController下有需要使用权限按钮的角色
//分配员角色
public function assignmentAction(Grid $grid)
{
//权限按钮
$grid->showActions();
$grid->showViewButton();
}
//在TaskController下有需要使用权限按钮的角色
//财务角色
public function financeAction(Grid $grid)
{
$grid->showActions();
$grid->showViewButton();
}
//在TaskController下有需要使用权限按钮的角色
//业务员角色
public function salesmanAction(Grid $grid)
{
}
//....其他角色
}
经过使用设计模式封装后,上面TaskController
中控制权限的代码直接优化成如下:【优雅了不少~
】
<?php
class TaskController extends BaseController
{
use HasResourceActions;
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
//Grid Columns...
//财务角色按钮
TaskAction::setAction($grid, AdminUserModel::getFinanceRole());
//分配员角色按钮
TaskAction::setAction($grid, AdminUserModel::getAssignmentRole());
//...其他角色按钮
/*
if (Admin::user()->inRoles([AdminUserModel::getAssignmentRole()])) {
$grid->disableBatchActions();
$grid->disableEditButton();
$grid->disableCreateButton();
$grid->disableDeleteButton();
} elseif (Admin::user()->inRoles([AdminUserModel::getEvaluatorRole()])) {
$grid->disableBatchActions();
$grid->disableEditButton();
$grid->disableCreateButton();
$grid->disableDeleteButton();
$grid->actions(function (Grid\Displayers\Actions $actions) {
$actions->append(new ConfirmCloseTaskAction());
});
} else {
$grid->disableCreateButton();
$grid->disableDeleteButton();
$grid->disableEditButton();
$grid->disableBatchActions();
$grid->disableViewButton();
$grid->disableActions();
}
*/
}
}