责任链模式 (ChainOfResponsibilites)

未匹配的标注

手握设计模式宝典 - 责任链模式 ChainOfResponsibilites

Design-Pattern - Behavioral - ChainOfResponsibilites

  • Title: 《手握设计模式宝典》 之 责任链模式 ChainOfResponsibilites
  • Tag: Design-PatternBehavioralChainOfResponsibilites责任链模式设计模式
  • Author: Tacks
  • Create-Date: 2023-08-17
  • Update-Date: 2023-08-17

0、REF

1、5W1H

1.1 WHAT 什么是责任链模式?

责任链模式 Chain-Of-Responsibilites 是一种行为设计模式 ,它允许你将请求沿着处理链进行传递,直到有一个处理器能够处理该请求。每个处理器都可以选择处理请求或将其传递给下一个处理器。

1.1.1 概念

在责任链模式中,有多个处理器对象组成一条链,每个处理器都有一个对下一个处理器的引用。当请求进入责任链时,从链的起点开始,每个处理器判断是否能够处理该请求。如果可以处理,则进行处理并结束链。如果不能处理,则将请求传递给下一个处理器,直到有一个处理器能够处理它。

  • [请求任务] Request
  • [客户端] Client
  • [责任链] Handler
  • [处理器对象] ConcreteHandler
                        组装[处理器对象]
                               |
[请求任务] ------> [客户端] ------> [责任链] ------> [结果]

1.1.2 真实世界类比

比如在公司内的审批流程,假设一个员工需要请假,那么就需要通过OA系统提交工单,经过一系列的审批,最终才能批准。
首先是员工将自己的诉求提交到OA上,也就是请假天数,放到这个请假的责任链上,后续的流程他无需再关系是谁批准。请假一天,那么可能组长就能批准,如果请假三天,可能组长就没有这个权限批准,需要进行上报到上一级的部门主管才可以,如果说请假100天,那么直接拜拜,无法批准。

  • 审批流程 => 责任链
    • 每个审批者,根据自己的权限和责任判断是否可以批准申请
    • 然后决定是否需要将申请传递到下一个审批者

1.1.3 特征

  • 多个处理器对象对同一个请求任务进行处理
  • 多个处理器对象形成链式存储,通过引用知道下一个处理器对象
  • 每个处理器对象的责任大概有这几种
    • 处理任务,结束任务
    • 不处理任务,传递给下一个处理器对象
    • 处理任务,并加一些操作,然后传递给下一个处理器对象
  • 客户端负责多个处理器对象的链式组装,形成责任链
  • 请求者,只需要扔个责任链就行至于是哪个处理器返回的无需关心

1.2 WHY 为什么有责任链模式?

1.2.1 解决了什么问题

责任链模式可以解决以下问题:

  • 解耦 发送者和接收者
    • 避免请求发送者和接受者耦合在一起 (不然发送者可能要调用多个处理器,然后一大堆 if/else
    • 发送者 => [责任链头部接收者] => 顺着链传递请求 => 结果
    • 责任链相对于发送者是黑盒 (发送者不需要知道请求将由哪个具体的处理器处理,也不需要直接与处理器交互)
  • 可扩展性:
    • 请求发送者,可以被多个处理器处理
    • 可以动态地组合和调整责任链中的处理器,比如新增或者调整顺序
  • 可维护性:
    • 每个处理器只关注自己的责任,易于理解和维护

1.2.2 带来了什么问题

责任链模式可能带来以下问题:

  • 性能影响:
    • 由于请求需要依次经过责任链中的处理器,处理链过长或处理器逻辑复杂可能会影响性能
    • 注意递归调用
  • 链路千万别成环
    • 类似链表一样,如果出现环状,那么可能处理器就在责任链里面出不来了

1.3 WHERE 在什么地方使用责任链模式?

  • 中间件/拦截器
    • 例如API请求中的限流、认证等中间件
  • try/catch
    • 多个 catch 异常捕获
  • 多级缓存

1.4 WHEN 什么时间使用?

具体可看实际问题有什么特征

  • 当有多个对象可以处理请求,并且需要动态决定由哪个对象来处理时
  • 当请求的发送者和接收者之间需要解耦,使得发送者不需要知道请求将由哪个处理器处理

1.5 WHO 谁负责调用?

在责任链模式中,客户端负责创建责任链,并将请求发送给链条的第一个处理器。之后,每个处理器负责决定是否处理该请求以及是否将请求传递给下一个处理器

1.6 HOW 如何去实现?

  • 定义一个抽象类 AbstractHandler
    • 持有对下一个处理器的引用 setNextHander($handle)
    • 处理请求的具体逻辑: 负责整个递归调用责任链上的处理器对象,判断是否处理和是否转发
    • 抽象处理方法 processing :需要子类的处理器进行实现
  • 具体处理器类 ConcreteHandler
    • 实现具体的处理方法:也就是各个处理器判断是否是自己的责任
    • 如果可以处理请求则处理,否则将该请求转给下一个
  • 客户端 Client
    • 创建责任链,接收请求

2、Code

2.1 利用链表保存责任链中的处理器的引用,实现购物时采用不同支付方式

例如在支付中,我们扣款顺序。首先可能根据不同条件,比如有些支付方式打折,那么就可以优先使用这种方式扣款,但是金额不足又要用另一种方式尝试,从而就形成了责任链。

2.1.1 抽象类

  • AcountAbstractHandler
    • setNextHandler() 保存对下一个处理器对象的引用
    • handlePay() 负责责任链的核心流程,递归的进行处理,每一层判断处理or传递
    • processing() 抽象方法,强制子类实现
/**
 * 设计模式:行为型-责任链模式
 * 
 * 场景:不同支付方式的顺序
 */

/**
 * @abstract AcountAbstractHandler
 * 
 * @var private $successor  下一个处理器
 * @var protected $balance 当前余额
 * @method public setNextHandler(AcountAbstractHandler $nextHandler) 设置责任链
 * @method public handlePay(float $costMoney) 定义了处理支付请求的基本逻辑 
 * @method abstract processing() 抽象具体实现
 */
abstract class AcountAbstractHandler {

     /**
     * 下一个处理器
     *
     * @var AcountAbstractHandler
     */
    private $successor = null; 

     /**
     * 余额
     *
     * @var float
     */
    protected $balance;


    public function __construct(float $money)
    {
        $this->balance = $money;
    }

    /**
     * 指向下一个处理器的引用
     *
     * @param AcountAbstractHandler $nextHandler
     * @return void
     */
    public function setNextHandler(AcountAbstractHandler $nextHandler)
    {
        $this->successor = $nextHandler;
    }

    /**
     * 处理支付逻辑
     *
     * @param float $costMoney 花费金额
     * @return bool 
     */
    final public function handlePay(float $costMoney) :bool
    {
        // 它首先调用当前处理器的 processing 方法
        $isProcessed = $this->processing($costMoney);

        if ($isProcessed) {
            // 如果处理成功,就直接返回
            echo sprintf("[%s] Success Paid %s", get_called_class(),  $costMoney). PHP_EOL;
            return true;
        } elseif ($this->successor) {
            // 如果处理失败,并且存在下一个处理器,则将请求传递给下一个处理器进行处理
            echo sprintf("[%s] Cannot Pay %s . Next Handler...", get_called_class(),  $costMoney). PHP_EOL;
            $isProcessed = $this->successor->handlePay($costMoney);
        } else {
            //  如果处理失败,并且不存在下一个处理器,则处理失败
            echo sprintf("[%s] Cannot Pay %s . We will send a text message to urge payment later...", get_called_class(),  $costMoney). PHP_EOL;
        }

        return false;
    }

    /**
     * 强制子类必须实现
     *
     * @param float $costMoney
     * @return void
     */
    abstract protected function processing(float $costMoney) :bool;
}

2.1.2 处理器

  • AcountAbstractHandler 抽象类
    • AlipayAcountConcreteHandler
      • 支付渠道1 (打八折)
      • 实现具体 processing() 支付责任
    • BankAcountConcreteHandler
      • 支付渠道2 (原价支付)
      • 实现具体 processing() 支付责任
/**
 * AlipayAcountConcreteHandler 具体业务处理
 */
class AlipayAcountConcreteHandler extends AcountAbstractHandler {

    /**
     * 支付打折
     *
     * @return float
     */
    public function discount() :float {
        return 0.8;
    }

    protected function processing(float $costMoney) :bool
    {
        $discounted = $costMoney * $this->discount();
        $result = $this->balance >= $discounted;
        if($result) {
            $this->balance -= $discounted;
            echo sprintf("[%s] 支付宝 Cost:%s Discounted:%s, Balance:%s . Continue...", get_class(),  $costMoney, $discounted, $this->balance). PHP_EOL;
        }
        return $result;
    }
}

/**
 * BankAcountConcreteHandler 具体业务处理
 */
class BankAcountConcreteHandler extends AcountAbstractHandler {

    protected function processing(float $costMoney) :bool
    {
        $result = $this->balance >= $costMoney;
        if($result) {
            $this->balance -= $costMoney;
            echo sprintf("[%s] 银行卡 Cost:%s , Balance:%s . Continue...", get_class(),  $costMoney, $this->balance). PHP_EOL;
        }
        return $result;
    }
}

2.1.3 客户端

// 定义支付顺序
$handler1 = new AlipayAcountConcreteHandler(99);
$handler2 = new BankAcountConcreteHandler(521);

// 形成责任链,会依次经过责任链中的处理器进行处理,直到找到能够处理请求的处理器或者到达责任链的末尾。
$handler1->setNextHandler($handler2);

// 买东西
$shopping = [1,10,30,100];
foreach ($shopping as $item) {
    $res = $handler1->handlePay($item);
    if (!$res) {
        echo "支付失败...";
        exit;
    }
}
echo "支付成功...";

2.2 利用 array_reduce() 构建责任链,用于中间件处理

想要利用 array_reduce() 实现 pipeline 管道,从而构造责任链架子,先了解一下闭包相关的知识。

这里主要是针对 Laravel 框架中,中间件部分实现做一个简单理解。

2.2.1 定义一组中间件

// 定义中间件
$middelwareA = function ($request, Closure $next) {
    echo 'middlewareA hanlde request' . PHP_EOL;
    if (!is_array($request)) {
        echo 'Exit(request is not array)' . PHP_EOL;
        exit;
    }
    if (empty($request['user'])) {
        echo 'Exit(request parameter does not have a user)' . PHP_EOL;
        exit;
    }
    return $next($request);
};
$middlewareB = function ($request, Closure $next) {
    echo 'middlewareB hanlde request' . PHP_EOL;
    if ($request['age'] < 18) {
        echo sprintf("Exit(%s's age < 18, not allowed to access)", $request['user']) . PHP_EOL;
        exit;
    }
    return $next($request);
};
$middlewareC = function ($request, Closure $next) {
    echo 'middlewarec hanlde request' . PHP_EOL;
    if ($request['money'] <= 0) {
        echo sprintf("Exit(%s's money <= 0, No amount, No access)", $request['user']) . PHP_EOL;
        exit;
    }
    return $next($request);
};

2.2.2 定义核心责任链方法

【核心】!!!敲重点哇!!!

  • carry()
    • 俩闭包,外层用来封装 array_reduce 所需要迭代的要求,内层用来构造像洋葱一样的闭包。
  • prepareDestination()
    • 一个闭包,用来处理最终的目的回调函数
function carry()
{
    return function ($stack, $pipe) {
        return function ($request) use ($stack, $pipe) {
            return $pipe($request, $stack);
        };
    };
}
function prepareDestination($callback)
{
    return function ($request) use ($callback) {
        echo "Callback handler request, Through a set of middleware. " . PHP_EOL;
        return $callback($request);
    };
}

2.2.3 调用闭包责任链

// 1、定义中间件组
$pipes = [$middelwareA, $middlewareB, $middlewareC];

// 2. 增加一个变量 request
$request = ['user' => 'tacks', 'age' => 18, 'money' => 1];

// 3. 增加一个目的地闭包
$callback = function ($request) {
    echo 'Come in' . PHP_EOL;
};

// 4. 利用 array_reduce 实现责任链
$func = array_reduce(array_reverse($pipes),  carry(), prepareDestination($callback));

// 5、调用闭包
$func($request);

// 输出
// middlewareA hanlde request
// middlewareB hanlde request
// middlewarec hanlde request
// Callback handler request, Through a set of middleware. 
// Come in
  • 大概闭包构造如下
/*
闭包构造责任链的顺序
  middlewareC
  middlewareB
  middlewareA
*/

// 大概执行闭包如下
/*

// 第一次闭包
function ($passable) {
    prepareDestination($passable, function ($passable) use ($callback) {
        return $callback($passable);
    });
}
// 第二次闭包
function ($passable) {
    middlewareC($passable, function ($passable) use ($callback) {
        return prepareDestination($passable, function ($passable) use ($callback) {
            return $callback($passable);
        });
    });
}
// 第三次闭包
function ($passable) {
    middlewareB($passable, function ($passable) use ($callback) {
        return middlewareC($passable, function ($passable) use ($callback) {
            return prepareDestination($passable, function ($passable) use ($callback) {
                return $callback($passable);
            });
        });
    });
}
// 第四次闭包
function ($passable) {
    middlewareA($passable, function ($passable) use ($callback) {
        return middlewareB($passable, function ($passable) use ($callback) {
            return middlewareC($passable, function ($passable) use ($callback) {
                return prepareDestination($passable, function ($passable) use ($callback) {
                    return $callback($passable);
                });
            });
        });
    });
}
// 调用闭包
Closure($passable)
*/

2.2.4 打印一下具体这个闭包嵌套

object(Closure)#9 (2) {
  ["static"]=>
  array(2) {
    ["stack"]=>
    object(Closure)#8 (2) {
      ["static"]=>
      array(2) {
        ["stack"]=>
        object(Closure)#7 (2) {
          ["static"]=>
          array(2) {
            ["stack"]=>
            object(Closure)#6 (2) {
              ["static"]=>
              array(1) {
                ["callback"]=>
                object(Closure)#4 (1) {
                  ["parameter"]=>
                  array(1) {
                    ["$request"]=>
                    string(10) "<required>"
                  }
                }
              }
              ["parameter"]=>
              array(1) {
                ["$request"]=>
                string(10) "<required>"
              }
            }
            ["pipe"]=>
            object(Closure)#3 (1) {
              ["parameter"]=>
              array(2) {
                ["$request"]=>
                string(10) "<required>"
                ["$next"]=>
                string(10) "<required>"
              }
            }
          }
          ["parameter"]=>
          array(1) {
            ["$request"]=>
            string(10) "<required>"
          }
        }
        ["pipe"]=>
        object(Closure)#2 (1) {
          ["parameter"]=>
          array(2) {
            ["$request"]=>
            string(10) "<required>"
            ["$next"]=>
            string(10) "<required>"
          }
        }
      }
      ["parameter"]=>
      array(1) {
        ["$request"]=>
        string(10) "<required>"
      }
    }
    ["pipe"]=>
    object(Closure)#1 (1) {
      ["parameter"]=>
      array(2) {
        ["$request"]=>
        string(10) "<required>"
        ["$next"]=>
        string(10) "<required>"
      }
    }
  }
  ["parameter"]=>
  array(1) {
    ["$request"]=>
    string(10) "<required>"
  }
}

2.3 利用数组存中间件充当责任链,使用 foreach 循环处理不叫作中间件吗

🐶 哈哈哈哈哈哈(doge , Laravel 中间件的调用实现确实优雅,优雅的看不懂,那就用简单的循环试试看。

2.3.1 定义中间件

// 中间件处理
function middelwareFunc($request)
{
    $request += 1;
    return $request;
}
// 控制器处理
function process($request)
{
    $response = $request * 10;
    echo sprintf("[process]: middelware end:%s", $request) . PHP_EOL;
    return $response;
}

// 中间件组
$pipes = array_fill(0, 10, 'middelwareFunc');

2.3.2 具体调用

// 具体调用
$request = 10;
echo sprintf("[request]: Start request:%s", $request) . PHP_EOL;
foreach ($pipes as $handle) {
    $request = $handle($request);
}
$response = process($request);
echo sprintf("[request]: End response:%s", $response) . PHP_EOL;

/*
[request]: Start request:10
[process]: middelware end:20
[request]: End response:200
*/

3、Application

3.1 Laravel 中间件 部分源码分析

中间件提供了一种机制来过滤进入应用程序的 HTTP 请求。过滤就是处理器的作用,多个中间件就提供了多个处理器处理同一请求的能力。

3.1.1 中间件基础使用

  • 创建中间件
php artisan make:middleware TokenValid
  • 注册中间件
// app\Http\Kernel.php
// 全局中间件
protected $middleware = [
    \App\Http\Middleware\TrustProxies::class                                ,   // 用于信任代理并设置受信任的代理 IP 地址
    \Fruitcake\Cors\HandleCors::class                                       ,   // 处理跨域资源共享(CORS)请求的中间件
    \App\Http\Middleware\PreventRequestsDuringMaintenance::class            ,   // 维护模式中的请求拦截中间件
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class         ,    // POST 请求大小验证中间件
    \App\Http\Middleware\TrimStrings::class                                 ,   // 对输入的字符串进行去除首尾空白字符的中间件
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,    // 将空字符串转换为 null 的中间件
];
// 路由中间件
protected $routeMiddleware = [];
// 中间件组
protected $middlewareGroups  = [];
  • 为路由分配特定中间件
use App\Http\Controllers\TestController;
use App\Http\Middleware\TokenValid;
Route::get('/test', [TestController::class, 'index'])->middleware(TokenValid::class)->name("test");
  • 为路由分配中间件组
// app\Http\Kernel.php
protected $middlewareGroups = [
    'api' => [
        // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

// routes\api.php
// 统一为 API 接口,增加 api 路由组
Route::group(['namespace' => 'Api', 'middleware' => 'api'], function () {
  Route::resource('articles', 'ArticleController');
}
  • 中间件顺序
// app\Http\Kernel.php
// vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php
protected $middlewarePriority = [];
  • 中间件类的基本定义
    • handle(Request $request, Closure $next)
    • $request 请求参数
    • $next 就是匿名函数/匿名类 Closure 的实例化对象
  • $next($request);
    • 这句代码的作用是将请求传递给下一个中间件或路由处理
    • 每个中间件都可以决定是否要终止请求或者将请求继续传递到下一个中间件
  • 示例中间件
    • 用来验证 token 是否匹配 helloworld , 否则重定向到 login
// app\Http\Middleware\TokenValid.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class TokenValid
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
     */
    public function handle(Request $request, Closure $next)
    {
        if ($request->input('token') !== 'helloworld') {
            return redirect('login');
        }
        return $next($request);
    }
}

3.1.2 从框架执行入口看起

  • public 入口文件

    问:kernel 框架内核中 handle() 方法做了哪些事情呢?

    答:如果不看框架源码,至少也知道肯定会经过 中间件、路由、控制器,但具体中间件是如何依次请求处理的呢?继续向下看。

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Kernel::class);

// 处理请求,返回响应
$response = $kernel->handle(
    $request = Request::capture()
)->send();
  • Illuminate\Foundation\Http\Kernelhandle() 方法
    • sendRequestThroughRouter($request) 核心方法,用来将请求通过路由器进行处理然后返回响应。
// vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php
/**
 * Handle an incoming HTTP request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function handle($request)
{
    try {
        // 请求方法覆盖(用来将某些不支持如 put delete的 http 请求方法 用的是 post 请求,但参数中包含如 _method=put,也能被转成正确的 put 请求)
        $request->enableHttpMethodParameterOverride();

        // [核心] !!! 敲重点 !!!
        $response = $this->sendRequestThroughRouter($request);
    } catch (Throwable $e) {
        // 上报异常
        $this->reportException($e);
        // 渲染异常对象
        $response = $this->renderException($request, $e);
    }
    // 请求处理结束事件
    $this->app['events']->dispatch(
        new RequestHandled($request, $response)
    );
    // 返回响应 (无论正常or异常)
    return $response;
}

3.1.3 框架请求处理的核心流程 Pipeline 浮出水面

[插播一下]
什么是管道 Pipeline ?
管道 是数据传递和处理的一种方法,通常是用于将多个处理步骤连起来,将每个步骤输入经过处理后的输出,传递到下一个步骤的输入,从而得到最终的输出结果。
管道 也可以看作流水线,每个阶段有特定的处理任务,利用管道将每一个阶段的处理结果传递给下一个阶段,从而实现流水线的分工,从而提高整体的效率和性能。

  • 管道的场景
    • Linux 的命令行管道 |
      • 比如 cat txt | grep 'hello' ,可以轻松实现多个命令的串联
    • CI/CD 的编排
      • 持续继承和持续交付中,管道用于构建、测试、部署等步骤
    • 中间件
      • 比如 Laravel 的 Middleware ,将请求经过多个中间件进行预处理
    • Redis 的批处理命令
      • 通过打包一批命令发送服务端,然后依次处理命令,从而减少 Round Trip Time (RTT 往返时间)

继续看 Laravel 的 中间件

  • Illuminate\Foundation\Http\KernelsendRequestThroughRouter() 方法
    • 通过 Pipeline 实例作为请求处理的管道,用于串联多个中间件,顺序执行对应的处理程序。
  • Illuminate\Pipeline
    • 作用:用于构建处理管道 Pipeline
    • 构造函数参数 : $this->app 提供给管道上下文
    • send() : 接收请求实例 $request
    • through() : 传入需要经过的中间件数组
    • then() :传入闭包,用于将请求派发给路由器
// vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php

/**
 * Send the given request through the middleware / router.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
protected function sendRequestThroughRouter($request)
{
    // 将当前请求实例绑定到应用程序容器中 (获取一个共享的实例)
    $this->app->instance('request', $request);

    // 清除已解析的 request 实例
    Facade::clearResolvedInstance('request');

    // 应用程序的初始化操作
        // [加载环境变量] \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        // [加载配置信息] \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        // [处理异常] \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        // [注册门面] \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        // [调用服务提供者的 register() 注册服务] \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        // [调用服务提供者的 boot() ] \Illuminate\Foundation\Bootstrap\BootProviders::class,
    $this->bootstrap();

    // 【核心】 !!!敲重点!!!
    return (new Pipeline($this->app))
    // 当前请求对象
                ->send($request)
    // 设置全局中间件数组!!! 此时的  $this->middleware 是 全局中间件 ,可以参考 (3.1.1 中间件基础使用)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
    // 闭包指定了将请求派发到路由器进行处理的逻辑
                ->then($this->dispatchToRouter());
}

3.1.4 Illuminate\Pipeline 管道的三链式操作

  • send()
    • 设置要经过管道的处理对象,例如 http 的 request
  • through()
    • 设置管道流水线要经过的处理器,例如 中间件
  • then() 核心
    • array_reduce() 构建责任链
      • array_reduce(array $array, callable $callback, mixed $initial = null): mixed 核心函数
        • 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值
      • array_reverse() 翻转中间件数组
        • 由于使用 array_reduce() 处理 中间件数组,每一个会返回上次的闭包,导致最终调用是类似栈的形式,所以要想按照预期顺序执行,需要先翻转数组;
    • $pipeline() 真正执行闭包
      • 没有这个是不会执行的 !!!
// vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php
// Illuminate\Pipeline
// 
/**
  * 设置通过管道发送的对象:请求对象 request
  *
  * @param  mixed  $passable
  * @return $this
  */
public function send($passable)
{
    $this->passable = $passable;

    return $this;
}

/**
  * 设置管道数组:通常如 中间件数组
  *
  * @param  array|mixed  $pipes
  * @return $this
  */
public function through($pipes)
{
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();

    return $this;
}
/**
  * 通过执行最终的回调函数,来执行 pipeline
  *
  * @param  \Closure  $destination
  * @return mixed
  */
public function then(Closure $destination)
{
    // 【核心】 !!!敲重点!!!
    $pipeline = array_reduce(
        array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
    );

    // 用来执行闭包
    return $pipeline($this->passable);
}

3.1.4 array_reverse() 函数的神奇用法-闭包套闭包

  • array_reduce(array $array, callable $callback, mixed $initial = null): mixed
    • $this->pipes() 就是中间件
    • $this->carry() 回调方法
    • $this->prepareDestination($destination) 初始值
array_reduce(
        array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
);
  • pipes()
    • 先前看到的 through() 方法,就是来设置中间件数组的,这里就是直接取中间件的数组
/**
  * Get the array of configured pipes.
  *
  * @return array
  */
protected function pipes()
{
    return $this->pipes;
}
  • carry()

    • 在迭代中间件数组的时候,进行回调处理的一个函数,核心就是调用每一个中间件的 handle() 方法

      问: 为什么你看到 carry() 里面有两个 return 闭包?
      答: 根据 array_reduce() 函数的用法,我们知道 第二个参数必定是回调函数 callback ,那么这里就采用 Closure 闭包,用匿名函数实现,所以第一个 return 是符合第二个参数的定义,并且 array_reduce() 规定的用法就是,callback(mixed $carry, mixed $item): mixedcarry 是上一次迭代返回值,item 是本次迭代值。
      至于第二个 return,就是 callback 回调函数的返回值,这里返回的也是 Closure 闭包,就问你晕不晕吧,闭包返回闭包,那么就会逐层嵌套进去,类似栈一样,最开始的一定在最里面,比喻成洋葱,那么被迭代的中间件数组第一个元素将会在洋葱中心。

      问: 为什么这里需要 stack ,接收上一层的迭代返回值?
      答: 可以接收上一层回调,并且处理当前迭代的中间件,这样整体才能形成嵌套链式,如果没有接收上一层回调,那么只会返回最后的一个闭包,而不是嵌套闭包。

// vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php
// Illuminate\Pipeline\Pipeline;
/**
 * The method to call on each pipe.
 *
 * @var string
 */
protected $method = 'handle';

/**
 * 返回的是一个闭包
 *
 * @return \Closure
 */
protected function carry()
{
    return function ($stack, $pipe) {
        // passable 就是当前 request
        // stack 中间件堆栈
        // pipe 当前要处理的中间件
        return function ($passable) use ($stack, $pipe) {
            try {
                if (is_callable($pipe)) {
                    // 如果中间件可以直接调用,比如是个闭包
                    return $pipe($passable, $stack);
                } elseif (! is_object($pipe)) {
                    [$name, $parameters] = $this->parsePipeString($pipe);

                    // 如果中间件是字符串,需要从依赖注入容器中解析出来,得到可调用对象
                    $pipe = $this->getContainer()->make($name);

                    // 把参数汇总
                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    // 如果中间件已经是一个对象,那么也可以直接调用
                    $parameters = [$passable, $stack];
                }
                // 调用中间件的方法,也就是 handle(), 传递参数,并获取结果
                $carry = method_exists($pipe, $this->method)
                                ? $pipe->{$this->method}(...$parameters)
                                : $pipe(...$parameters);
                // 返回中间件结果
                return $this->handleCarry($carry);
            } catch (Throwable $e) {
                return $this->handleException($passable, $e);
            }
        };
    };
}
  • prepareDestination()
    • 用于准备最终的闭包函数,在中间件的最后一层执行目标操作,在此处的目的,比如经过中间件的处理验证后,需要识别路由,执行控制器的动作,等
/**
 * Get the final piece of the Closure onion.
 *
 * @param  \Closure  $destination
 * @return \Closure
 */
protected function prepareDestination(Closure $destination)
{
    return function ($passable) use ($destination) {
        try {
            return $destination($passable);
        } catch (Throwable $e) {
            return $this->handleException($passable, $e);
        }
    };
}

3.1.5 $this->dispatchToRouter() 派发路由器的工作

任务大致流程: 具体可以查看源码 Illuminate/Routing/Router

  • 特定路由识别
    • gatherRouteMiddleware 定位路由、解析对应路由的特定中间件、中间件组、控制器中间件、一些需要排除的中间件、根据中间件名称解析出对应实例、中间件排序。
  • Pipeline
    • runRouteWithinStack 将请求再次经过特定中间件处理一波,这里类似上面处理全局中间件的流程
  • 执行控制器方法
    • run()
  • 格式化响应信息
    • prepareResponse()

3.1.6 Laravel 中间件小结

Laravel中间件 Middleware 提供了一种很方便过滤用户请求 request 的机制。通过定义一批中间件组,来依次经过处理用户请求,这个过程和 责任链 模式 有异曲同工之处。

抛开概念来说,责任链模式就是让请求经过责任链上的一系列处理器,将接收者和处理器解耦。在 Laravel 中间件上同样体现了这一点,所以我觉得,这个是可以当作责任链的一个实践案例的,至于具体的实现方案,有很多种,比如用迭代依次处理责任链,用 Pipeline管道/流水线 来处理等等。

Laravel 中间件形成一个责任链,每个中间件都有机会在请求前或请求后进行处理。请求按照定义的顺序经过中间件链,每个中间件可以选择中断请求的传递或者继续将请求传递给下一个中间件。

Laravel 中间件具体使用的 Pipeline 来实现责任链的调用,比如全局中间件的调用过程如下

  1. 定义中间件的顺序;
    1. config/app.php$middleware 数组,定义全局中间件,会按照顺序被执行
  2. 中间件责任链;
    1. sendRequestThroughRouter() 方法负责 Pipeline 实例创建
    2. Pipeline 的 send() 用来设置 request 请求对象
    3. Pipeline 的 through() 用来设置中间件数组
    4. Pipeline 的 then() 形成责任链进行闭包调用
  3. 中间件执行;
    1. 每一个中间件的 handle() 方法,用来接收请求,处理后,决定是否传递 $next() 给下一个中间件

3.2 关于 array_reduce() 的了解

插播一下
主要是针对 array_reduce() 一些的理解

3.2.1 场景1 - 回调函数的返回类型是 int

// callback 回调函数的返回类型是 int
$arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
$func1 = array_reduce($arr1, function ($carry, $item) {
    $carry += $item;
    return $carry;
});
var_dump($func1); // 55

3.2.2 场景2 - 回调函数的返回类型是 闭包, 闭包 use 本次的携带值 item

array_reduce 的参数二 回调函数 callback 的返回类型是 闭包,并且闭包 use item,那么每次都是接收本次的携带值 item,不会用到上一次的 carry
也就是每一轮返回的都是 use 当前 item 的闭包,数组迭代完毕后,最终返回的是 use 最后元素的闭包

$arr2 = ['APPLE', 'BANANA', 'CHERRY'];
$func2 = array_reduce($arr2, function ($carry, $item) {
    // debug 
    // var_dump($carry);
    // var_dump($item);
    return function () use ($item) { //这里只use了item
        return strtolower($item);
    };
});
var_dump($func2());
// `array_reduce()` 的执行流程
// [1] carry = null ; item = APPLE
// [2] carry = Closure use APPLE ; item = BANANA
// [3] carry = Closure use BANANA ; item = CHERRY
// [4] array_reduce 将数组转化到最后得到闭包 Closure use CHERRY ;  
// [5] 执行闭包 $func2() =>  function() use ('CHERRY'){ return strtolower('CHERRY'); }(); => 'cherry'

3.2.3 场景3 - 回调函数的返回类型是 闭包, 闭包 use 本次的携带值 item 和 上次的返回值 carry

$arr3 = ['ONE', 'TWO', 'THREE'];
$func3 = array_reduce($arr3, function ($carry, $item) {
    // debug 
    var_dump($carry);
    var_dump($item);
    return function () use ($carry, $item) {
        if (is_null($carry)) {
            return 'Carry is NULL, Item=' . $item;
        }
        if ($carry instanceof Closure) {
            return $carry() . $item;
        }
    };
});
var_dump($func3());


// `array_reduce()` 的执行流程
// [1] carry = null ; item = ONE 
// [2] carry = Closure use NULL,ONE ; item = TWO
// [3] carry = Closure use (Closure use NULL,ONE),TWO ; item = THREE
// [4] array_reduce 将数组转化到最后得到闭包 Closure use (Closure use (Closure use NULL,ONE),TWO), THREE ;  
// [5] 执行闭包 $func3() 
/*
    (Closure use (Closure use (Closure use NULL,ONE),TWO), THREE)()

    =>

    function() use ($carry, $item){
        if(is_null($carry)) {
            return 'Carry is NULL, Item='. $item;
        }
        if($carry instanceof Closure) {
            return $carry() . $item;
        }
    };

    => Carry is NULL, Item=ONETWOTHREE
*/

3.3 用 array_reduce 实现简易 Pipeline ,理解中间件原理

/**
 * Pipeline 管道简易实现
 */
class Pipeline
{

    protected $passable;  // 请求对象
    protected $pipes = [];// 中间件

    // 设置请求对象
    public function send($passable)
    {
        $this->passable = $passable;
        return $this;
    }

    // 设置中间件
    public function through($pipes)
    {
        $this->pipes = is_array($pipes) ? $pipes : func_get_args();
        return $this;
    }

    // 设置责任链,并且调用闭包执行
    public function then(Closure $destination)
    {
        $pipeline = array_reduce(
            array_reverse($this->pipes()),
            $this->carry(),
            $this->prepareDestination($destination)
        );
        return $pipeline($this->passable);
    }

    // 获取中间件
    function pipes()
    {
        return $this->pipes;
    }

    // 迭代中间件构造责任链闭包
    function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                // 如果不是闭包,简单构造一下,可以不需要
                if(!is_callable($pipe)) {
                    $pipe = function ($parameter, $next) { return $next($parameter); };
                }
                return call_user_func($pipe, $passable, $stack); // 对应中间件 pipe($passable, $stack)
            };
        };
    }

    // 最终目的地调用
    function prepareDestination($dest)
    {
        return function ($passable) use ($dest) {
            return call_user_func($dest, $passable);
        };
    }
}

/**
 * 简易中间件方法
 *
 * @param $parameter 参数
 * @param $next 闭包
 * @return 
 */
function middlewareHandle($parameter, $next)
{
    $parameter++;
    echo sprintf("[%s] count:%s", __FUNCTION__, $parameter) . PHP_EOL;
    return $next($parameter);
}

// 通过中间件组的目的方法
$process = function ($request) {
    echo sprintf("[%s] request:%s", __FUNCTION__, $request) . PHP_EOL;
};


// 批量构造中间件组
$middleware = array_fill(0, 10, 'middlewareHandle');
// 追加最后一个
$middleware[] = function ($parameter, $next) {
    $parameter -= 100;
    echo sprintf("[中间件结束] count:%s", $parameter) . PHP_EOL;
    return $next($parameter);
};

// 追加第一个
array_unshift($middleware, function ($parameter, $next) {
    echo sprintf("[中间件开始] count:%s", $parameter) . PHP_EOL;
    return $next($parameter);
});

$request = 0;
(new Pipeline())
    ->send($request)
    ->through($middleware)
    ->then($process);

4、 Summary

责任链模式,将请求与处理器解耦,让请求可以处理器责任链中传递和处理。 ,本质上,责任链就是形成链表结构,让每个节点的处理器可以选择处理或者转发请求给下一个处理器。

  • 为什么要用责任链模式?
    • 可以不用的,if/else 大法也不错
    • 为了扩展性和灵活性更好,而不需要每次去客户端调用的地方多谢一个处理器的 if/else
  • 处理器要负责什么内容?
    • 处理请求
    • 传递请求
  • 有什么需要注意的责任链模式?
    • 链式避免成环
    • 递归操作避免嵌套过深
    • 责任链的末尾最好有兜底操作
  • Laravel 的 中间件用什么实现的?
    • 主要用 Pipeline ,具体是 array_reduce() 核心函数
    • 利用闭包的封装,实现像洋葱一样一层层剥开的责任链

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~