装饰者模式在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类实现的,感兴趣的朋友可以参考下源码。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容