中间件与责任链模式

前段时间前同事找我看了这么一段代码:

(new Pipeline($this->container))
                ->send($passable)
                ->through($middleware)
                ->then($destination);

是laravel框架里看起来很直观的一段调用逻辑,把container注入到Pipeline对象,然后将passable(其实一般是$request对象)发送通过一连串的中间件,最后到达目的地(最终的processHandler之类的函数)。

看起来很不错是不是,再看看laravel内部的实现,代码在Illuminate\Pipeline下:

class Pipeline implements PipelineContract
{
    protected $container;

    /**
     * The object being passed through the pipeline.
     *
     * @var mixed
     */
    protected $passable;

    /**
     * The array of class pipes.
     *
     * @var array
     */
    protected $pipes = [];

    public function send($passable)
    {
        $this->passable = $passable;
        return $this;
    }

   public function then(Closure $destination)
    {
        $firstSlice = $this->getInitialSlice($destination);

        $pipes = array_reverse($this->pipes);

        return call_user_func(
            array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passabl
        );
    }

    protected function getSlice()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                // If the pipe is an instance of a Closure, we will just call it
                // otherwise we'll resolve the pipes out of the container and ca
                // the appropriate method and arguments, returning the results b
                if ($pipe instanceof Closure) {
                    return call_user_func($pipe, $passable, $stack);
                } else {
                    list($name, $parameters) = $this->parsePipeString($pipe);

                    return call_user_func_array([$this->container->make($name),
                            array_merge([$passable, $stack], $parameters));
                }
            };
        };
    }

    protected function getInitialSlice(Closure $destination)
    {
        return function ($passable) use ($destination) {
            return call_user_func($destination, $passable);
        };
    }

代码看起来很短,但是非常不好理解,我们把他简化一下:

function hello($parameter, $next) {
    echo "hello\n";
    return $next($parameter);
}

function process() {
    echo "process\n";
}

$middleware = ["hello"];

function getSlice() {
    return function($stack, $pipe) {
        return function($passable) use ($stack, $pipe) {
            return call_user_func($pipe, $passable, $stack);
        };
    };
}

function firstSlice($dest) {
    return function() use ($dest) {
        return call_user_func($dest);
    };
}


call_user_func(
    array_reduce(
        array_reverse($middleware),
        getSlice(),
        firstSlice("process", "")
    )
    , ""
);

array_reduce是php特有的一个函数,会将每一次运算的结果和第一个参数里的数组的下一个元素作为函数变量的两个参数进行循环叠代,直至对整个数组完成遍历。这里的数组当然也就是指我们的middleware。

这里我们的middleware只有一个元素,所以array_reduce的任务就是拿firstSlice函数返回的闭包:

function() use ("process") {
    return call_user_func("process");
};

=>相当于代码里的stack

作为第一个参数,"hello" =>相当于代码里的pipe,作为第二个参数,调用getSlice,

得到:

function("process的闭包", "hello") {
        return function("可以传入的参数") use ("process的闭包", "hello") {
            return call_user_func("hello", "可以传入的参数", "process的闭包");
        };
}

这时候调用call_user_func,那么就相当于对这个闭包执行计算了。

先来看看reduce出来是一个什么样的结构:

object(Closure)#1 (2) {
  ["static"]=>
  array(2) {
    ["stack"]=>
    object(Closure)#2 (1) {
      ["static"]=>
      array(1) {
        ["dest"]=>
        string(7) "process"
      }
    }
    ["pipe"]=>
    string(6) "hello1"
  }
  ["parameter"]=>
  array(1) {
    ["$passable"]=>
    string(10) "<required>"
  }
}

闭包套闭包,实在是蛋疼。

如果middleware数组的内容再多一些的话,其实就是从这个闭包向外继续包裹闭包,所以实际上最终形成的是下面这样的结构:

----- process
----- param3 = hello2(param2)
--- hello2
--- param2 = hello1(param1)
- hello1
- param1

在最终的call_user_func的时候是一层一层向上执行的。

也有人把这个比作是剥洋葱的过程。

非常精妙对不对?

屁啊!

下面我来给你讲讲为什么这个pipeline不靠谱。

我们先来分析他的目的,这个array_reduce实际上了是为了顺序调用middleware,然后最终去处理processHandler的逻辑。

这个实际上就是设计模式里的责任链模式,或者叫流水线模式,实际上是非常简单的一个东西。看名字也能明白是什么意思了。

当然了,想要实现这个目的其实很简单:

$request; //源请求
foreach($middlewares as $middlware) {
    $request = $middleware($request);
}

$response = $process($request);

当然有人会说了,你这个for循环也太low了,我们这么写就是为了减少for循环的次数而且还可以让别人看不懂

那么我们再来分析一下,这个Pipeline还有没有其它的问题。

当我们看了框架的源码以后,理所当然的会觉得自己的战斗力又+5,想要立刻拿这个现学的设计模式去完成一些我们自己的业务需求。

比如在这里我们虚构一个需求:

我们有一个非常复杂的机器需要制造,大概要经过60000道工序,所有的工序都是串连的,每一道工序的输出结果都需要传到流水线的下游继续进行一些处理。那么你怎么设计一个程序来满足这个需求呢?

还用前面的laravel式pipeline来试一试,我把示例代码放在了:

https://github.com/cch123/test/blob/master/php/pipeline_stacktest.php

读者桑可以把for循环里的700改成60000试一试。为了简单,我们这里估且先进行6w道同样的工序(逃

执行一下代码:

segmentation fault

坑不坑爹,就问你坑不坑爹?

这件事情说明了一个问题,php的闭包套闭包在函数调用的时候类似于递归调用,也存在的压栈压爆的问题。

所以想要实现一个可以泛用的pipeline,根本不需要参考laravel这种坑爹的实现方式。在laravel这个框架的其它地方,实际上类似的情况也有直接用foreach来解决问题的,这不由地让人觉得laravel的开发组成员是不是有点人格分裂(当然也未必就是同一个人写的代码)。况且这种php框架的中间件数目也不会太多,不会说就遇上我们这种6w道工序的坑爹需求。

但这些不是把代码写复杂难懂的理由。

我们再去看看另一种pipeline的实现:

https://github.com/thephpleague/pipeline

是不是觉得人生都敞亮起来了?

没错,拿技术解决问题,但是不要为了炫技而炫技。(特别是你的特技可能连一个“丑陋”的解决方案都打不过。