装饰者模式在Laravel框架中的实现思路
1、装饰者模式
装饰者模式是在开放——关闭原则下实现动态添加或减少功能的一种方式。
说明:装饰者模式就是不修改原类代码和继承的情况下动态扩展类的功能。传统的编程模式都是子类继承父类实现方法重载,使用装饰器模式,只需添加一个新的装饰器对象,更加灵活,避免类数量和层次过多。
以Laravel框架为例,在解析请求生成响应之前或之后需要经过中间件的处理,主要包括验证维护模式、Cookie加密、开启会话、CSRF保护等,而这些处理有些是在生成响应之前,有些是在生成响应之后,在实际开发过程中有可能还需要添加新的处理功能,如果再不修改原有类的基础上动态地添加或减少处理功能将使框架可扩展性大大增强, 而这种需求正好可以被装饰者模式解决。下面简单的给出一个装饰者模式的示例。
<?php
interface Decrator
{
public function display();
}
/*定义一个装饰者xiaofang*/
class XiaoFang implements Decrator
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function display()
{
echo "我是".$this->name."我出门了!!"."<br/>";
}
}
/*定义一个服饰类*/
class Finery implements Decrator
{
private $component;
public function __construct(Decrator $component)
{
$this->component = $component;
}
public function display()
{
$this->component->display();
}
}
/*定义一个鞋子类*/
class Shoes extends Finery
{
public function display()
{
echo "穿上鞋子"."<br>";
parent::display();
}
}
/*定义一个裙子类*/
class Skirt extends Finery
{
public function display()
{
echo '穿上裙子'."<br/>";
parent::display();
}
}
/*定义一个发型类*/
class Hair extends Finery
{
public function display()
{
echo '出门前先整理头发'.'<br/>';
parent::display();
echo '出门后再整理一下头发'."<br/>";
}
}
$xiaofang = new XiaoFang('小芳');
$shoes = new Shoes($xiaofang);
$skirt = new Skirt($shoes);
$hair = new Hair($skirt);
$hair->display();
// 输出:
// 出门前先整理头发
// 穿上裙子
// 穿上鞋子
// 我是小芳,我出门了
// 出门后再整理一下头发
?>
我们假设小芳接到一个电话算是请求,而小芳出门是对请求的响应,那么在小芳出门前后要对自己进行打扮,对应于Laravel框架中,这些打扮的步骤就相当于中间件的功能,而小芳出门是对请求的真正响应。在小芳的打扮的过程中,可以随时增加新的打扮类,只要该类继承Finery类(装饰类)并调用父类的同名方法,就可以实现重新组织打扮过程,实现打扮步骤的增加或减少,例如加一件衣服、化个妆等。这就是装饰者模式的应用场景。
2、请求处理管道
如上所示增加或者减少功能需要重新组织相应过程,比如new Shoes 和 new Skirt顺序互换,也就是实例化相应类的顺序,我们在这里是手动实现的,但是在laravel中我们知道它是通过服务容器进行自动实例化的,它的服务容器就是解决这些依赖注入的自动化设备,实例之间的功能调用也是通过闭包函数完成的,这里为了将问题简单化,我们通过静态函数来避免实例化的过程,只模拟一下通过闭包函数来完成装饰者模式,实现请求的处理管道。在Laravel框架中,针对请求的处理过程一共使用三次处理管道,我们先来看一段管道代码:
<?php
/*定义一个 中间件接口*/
interface Middllware
{
public static function handle(Closure $next);
}
/*定义一个 csrf验证类*/
class VerifyCsrfToken implements Middleware
{
public static function handle(Closure $next)
{
echo "验证Csrf-Token"."<br/>";
$next();
}
}
/*定义一个 错误分享类*/
class ShareErrorsFromSession implements Middleware
{
public static function handle(Closure $next)
{
echo "如果session中有'errors'变量,则共享它"."<br/>";
$next();
}
}
/*定义一个 开启session类*/
class StartSession implements Middleware
{
public static function handle(Closure $next)
{
echo "开启session,获取数据"."<br/>";
$next();
echo "报错数据,关闭session"."<br>";
}
}
/*定义一个 添加cookie队列至响应 类*/
class AddQueuedCookiesToResponse implements Middleware
{
public static function handle(Closure $next)
{
$next();
echo "添加下一次请求需要的cookie"."<br/>";
}
}
/*定义一个 加密cookie类*/
class EncryptCookies implements Middleware
{
public static function handle(Closure $next)
{
echo "对输入请求的cookie进行解密"."<br/>";
$next();
echo "对输出响应的cookie进行加密"."<br/>";
}
}
/*定义一个 检测维护状态类*/
class ChecKForMaintenanceMode implements Middleware
{
public static function handle(Closure $next)
{
echo "确定当前程序是否处于维护状态"."<br/>";
$next();
}
}
function getSlice()
{
return function($stack $pipe)
{
return function() use ($stack $pipe)
{
return $pipe::handle($stack);
};
};
}
function then()
{
$pipes = [
"ChecKForMaintenanceMode",
"EncryptCookies",
"AddQueuedCookiesToResponse",
"StartSession",
"ShareErrorsFromSession",
"VerifyCsrfToken"
];
$firstSlice = function() {
echo "请求向路由器传递,返回响应"."<br/>";
};
$pipes = array_reverse($pipes);
call_user_func(array_reduce($pipes, getSlice(), $firstSlice));
// array_reduce()向用户自定义函数发送数组中的值,并返回一个字符串:
then();
}
/**输出:
* 确定当前程序是否处于维护状态
* 对输入请求的cookie进行解密
* 开启session,获取数据
* 如果session中有'errors'变量,则共享它
* 验证Csrf-Token
* 请求向路由器传递,返回响应
* 保存数据,关闭session
* 添加下一次请求需要的cookie
* 对输出响应的cookie进行加密
*/
?>
我们来看一下这个函数array_reduce($arr, $callback, initial) 使用回调函数迭代地将数组简化为单一的值。
其中$arr为输入的数组,$callback($result, $value)接受两个参数,$result为上一次迭代产生的值,$value是当前迭代的值。简单地讲就是向用户自定义函数发送数组中的值,并返回一个字符串。使用array_reduce()替代foreach()循环最常用的业务场景也许就是数组求和了,比如:
- 注意 如果指定第三个参数,则该参数将被当成是数组中的第一个值来处理,或者如果数组为空的话就作为最终返回值。
<?php
$arr = array('1', '2', '3');
$sum = 0;
foreach ($arr as $v) {
$sum += $v; // echo $sum 求和完成
}
echo array_reduce($arr, function ($result, $v) {
return $result + $v; // 使用array_reduce()迭代求和
})
?>
再比如,在某些业务场景下,我们从数据库中查询出一组数据,需要将他们拼接成类似 (1,2,3,4,5)的 字符串,然后在 “SELECT * WHERE id in(1,2,3,4,5) ” 处理,这时候完全可以 foreach() 数组处理,其实也可以使用 array_reduce(),因为 array_reduce()就是“迭代地将数组简化为单一的值”,如下
$arr = array(
array("id"=>1,'name'=>"a"),
array("id"=>2,"name"=>"c"),
array("id"=>3,"name"=>"d")
);
echo array_reduce($arr , function ($result, $v) {
return ltrim($result.','.$v['id'],',');
});
回到之前的代码可以看到输出的内容是laravel框架对请求处理的部分流程,大部分与我们上面的装饰者模式形式是不是很相似,但以上通过回调函数生成整个处理流程的过程还是比较难以理解的,我们这里把它简单化,用一个简单的实例演示下,代码如下:
<?php
interface Step
{
public static function go(Closure $next);
}
class FirstStep implements Step
{
public static function go(Closure $next)
{
echo "开启session,获取数据"."<br/>";
$next();
echo "保存数据,关闭session"."<br/>";
}
}
function goFun($step, $className)
{
return function () use ($step, $className)
{
return $className::go($step);
};
}
function then()
{
$step = ['FirstStep'];
$prepare = function () {echo "请求向路由传递,返回响应"."<br/>"};
$go = array_reduce($step, "goFun", $prepare);
$go();
}
then();
输出:
开启session,获取数据
请求向路由传递,返回响应
保存数据,关闭session
?>
以上代码我们将处理功能简化为只需要一步,再次回到这个array_reduce($steps, "goFun", $prepare)函数和goFun($step, $className)函数上,通过前面的介绍我们已经知道array_reduce()接收三个参数,其中最后一个参数为可选参数,也叫initial(初始)参数,他会被当做数组中第一个值来处理,如果数组为空则作为返回值。而我们在这里给第三个参数传递的是一个回调函数$prepare,它将请求向路由器继续传递,返回我们需要的响应,而第一个参数数组则记录了外层功能的类名,goFun()函数作为处理数组的回调函数。那么很明显,array_reduce()最终返回的仍是一个回调函数,即$go():
$go = function () {
return $FirstStep::go(function () {echo "请求向路由传递,返回响应"."<br/>";});
};
总结:
之前的例子中,通过call_user_func()函数执行这个回调函数,其实就相当于$go()的自我调用。以上我们应该很清晰地理解了装饰者模式以请求处理管道的方式实现的设计思路了。通过将复杂的东西简化一下便于我们更好的理解。在Laravel框架中,请求处理管道主要是通过Illuminate\Pipeline\Pipeline类实现的,感兴趣的朋友可以参考下源码。