Pipeline管道模式,也有人叫它裝飾模式。應該說管道是裝飾模式的一個變種,雖然思想都是一樣的,但這個是閉包的版本,實現方式與傳統裝飾模式也不太一樣。在laravel的源碼中算是一個比較核心的設計模式了。
管道模式,或者說裝飾模式的思想,就是在不改變原有程序的基礎上,可以方便的在已有程序上添加新的功能。
在說管道模式之前讓我們看一下array_reduce這個函數
mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) array_reduce() 將回調函數 callback 迭代地作用到 array 數組中的每一個單元中,從而將數組簡化為單一的值。 array 輸入的 array。 callback mixed callback ( mixed $carry , mixed $item ) carry 攜帶上次迭代里的值; 如果本次迭代是第一次,那么這個值是 initial。 item 攜帶了本次迭代的值。 initial 如果指定了可選參數 initial,該參數將在處理開始前使用,或者當處理結束,數組為空時的最后一個結果。
以上是php官網對這個函數的解釋,看的一頭霧水對不對?沒關系,我們先來一個demo
//首先我們有一個數組 $a = array(1, 2, 3, 4, 5); $x = array(); //我們輸出了array_reduce的結果為15,這個函數傳遞了兩個參數,第一個就是上面的數組a,我們重點來看第二個閉包函數 var_dump(array_reduce($a, "sum")); // int(15) //array_reduce的第三個參數會在循環開始的時候當做閉包函數的第一個carry值傳入 var_dump(array_reduce($a, "product", 10)); // int(1200), because: 10*1*2*3*4*5 //當array_reduce處理結束后,若結果為空,也會將第三個參數返回 var_dump(array_reduce($x, "sum", "No data to reduce")); // string(17) "No data to reduce" //這個閉包函數接收了兩個參數 function sum($carry, $item) { echo 'before___carry:'.$carry.'<br><br>'; echo 'before___item:'.$item.'<br><br>'; //這個函數只是簡單的對兩個參數做了加法 $carry += $item; echo 'after___carry:'.$carry.'<br><br>'; echo '<br><br>'; return $carry; } function product($carry, $item) { echo 'before___carry:'.$carry.'<br><br>'; echo 'before___item:'.$item.'<br><br>'; $carry *= $item; echo 'after___carry:'.$carry.'<br><br>'; echo '<br><br>'; return $carry; }
大家在運行過這個demo之后,便會發現,array_reduce只是一個循環函數。但是,它和foreach不一樣的地方在於,它的閉包處理函數所接收的兩個參數。其中的$item參數會在每次循環的時候代入數組的各項值,而$carry就比較專一了,它只接收上一次循環中自己的返回值。不過專一也只是相對的,$carray還接收array_reduce傳入的第三個參數作為閉包開始循環前$carray的默認值,或是循環結束后返回值為null時的默認值。我在代碼中已經把變量各時期的值給打印出來了,相信大家一看就會明白。這個函數最典型的例子就是在做累計運算的時候,我們不需要向foreach循環那樣創建一個變量了。(大家都知道給變量起名字是一件很麻煩的事情)
ok,函數介紹完了,可是這和管道模式又有什么關系呢?不知道大家注意到沒有,在array_reduce的閉包函數進行循環的時候,array_reduce的第三個參數從閉包的$array進入,從return返回,緊接着再次從$array中進入,而每次循環的時候,都會從$item中獲取到外部進行運算。這個過程是不是很像數據在一個螺旋管道中流通,同時不斷的向其中添加新的操作,並且沒有改變原先的程序代碼,降低了程序間的耦合度。
接下來進行管道模式
請求處理管道的思維導圖
思維導圖看的還不夠清晰?ok,接着舉例子,看demo
從圖中我們可以知道類Request都具有相同的接口,那么我們可以約定一個接口規范
interface RequestInterface { // 我們之前講過裝飾者模式,這里是通過閉包函數實現 // 通過之后實現類及調用就可以看出 public static function handle(Closure $next); }
接口有了那么我們就遵循接口開始實現
class Request1 implements RequestInterface { public static function handle(Closure $next) { echo "Request1 Begin." . "<br />"; $next(); echo "Request1 End." . "<br />"; } } class Request2 implements RequestInterface { public static function handle(Closure $next) { echo "Request2 Begin." . "<br />"; $next(); echo "Request2 End." . "<br />"; } } class Request3 implements RequestInterface { public static function handle(Closure $next) { echo "Request3 Begin." . "<br />"; $next(); echo "Request3 End." . "<br />"; } } class Request4 implements RequestInterface { public static function handle(Closure $next) { echo "Request4 Begin." . "<br />"; $next(); echo "Request4 End." . "<br />"; } }
四種請求處理過程我們均已實現,為了簡化都是打印一句話,可以在瀏覽器上直觀顯示流程;
我們還需要一個客戶端來發出請求
class Client { // 這里包含了所有的請求 private $pipes = [ 'Request1', 'Request2', 'Request3', 'Request4', ]; // 這里就是思維導圖中默認返回的匿名回調函數 private function defaultClosure() { return function () { echo '請求處理中...' . "<br />"; }; } // 這里就是整個請求處理管道的關鍵 private function getSlice() { return function ($stack, $pipe) { return function () use ($stack, $pipe) { return $pipe::handle($stack); }; }; } // 這里是負責發起請求處理 public function then() { call_user_func(array_reduce($this->pipes, $this->getSlice(), $this->defaultClosure())); } }
當我們調用時:
$worker = new Client(); $worker->then();
瀏覽器就會顯示:
那么我來解釋一下整個流程吧,在代碼注釋說了getSlice
是關鍵,那么我們來解讀一下代碼
解析代碼
client的then方法,call_user_func是執行一個函數的api,它的參數是一個閉包函數。而這個參數則是我們文章一開始提到的array_reduce這個循環的最終返回值。
我們都還記得這個函數一共有三個參數:
第一個參數代表從外界加入到管道中的附加操作
第二個參數則是執行加工的工廠
第三個參數則是進入管道的程序數據源
先看第三個參數數據源
// 這里就是思維導圖中默認返回的匿名回調函數 private function defaultClosure() { //這個return出去的閉包就是我們的數據源 return function () { echo '請求處理中...' . "<br />"; }; }
然后對程序進行裝飾的裝飾品數組
// 這里包含了所有的請求 private $pipes = [ 'Request1', 'Request2', 'Request3', 'Request4', ];
這里雖然只是個數組,但它在剛剛的循環閉包中被當做對象名來使用,通過名稱調度不同的類
然后是循環閉包
// 這里就是整個請求處理管道的關鍵
private function getSlice()
{
//這個函數我們的閉包循環函數
return function ($stack, $pipe) { //而這里便是我們最開始例子中的return返回值,只不過剛開始的時候我們返回的是一個數字變量,這里變成了另一個閉包 return function () use ($stack, $pipe) { return $pipe::handle($stack); }; }; }
這里的循環閉包返回值也是一個閉包,在不斷的循環中,閉包函數就像洋蔥一樣,一層一層不斷包裹。
如此類推,最終array_reduce返回一個匿名函數,這就像上面思維導圖最終所描述的一樣
function (){ return Request4::handle(function (){ return Request3::handle(function (){ return Request2::handle(function (){ return Reuqest1::handle(function (){ echo '請求處理中...' . "<br />"; }); }); }); }); }
所以當調用call_user_func時就是執行array_reduce返回的匿名函數,我們從各個請求處理類的handle方法也得知,會直接調用傳入的匿名函數,注意順序即可理解瀏覽器的輸出。
與裝飾者模式的關系
這里沒有用到類來對實例對象進行包裝,而是通過閉包函數完成,每一次處理在上一次處理之上進行包裝,最后獲得響應,就像流水線一樣,一個請求進來通過一道道工序加工(包裝)最后生成響應。
理解了這幾個demo,那么laravel里中間件最重點的部分你就已經理解了。
最后,本文內容並非全部原創,部分圖片與代碼來源:http://blog.chenjunwu.cn/2017/05/02/Request-pipeline