express洋葱模型

前言 因为对框架源码的生疏,笔者最近在看博文视点的陈昊的《Laravel框架关键技术解析》。 看到书中反复提及的管道处理,写下一些自己的所思所想。 书中一直在描述Laravel框架如何优雅,例如在第6章<Laravel框架中的设计模式>中写这个管道处理使用了装饰器模式,很elegant;在第7章<

前言

因为对框架源码的生疏,笔者最近在看博文视点的陈昊的《Laravel框架关键技术解析》。

看到书中反复提及的管道处理,写下一些自己的所思所想。

书中一直在描述Laravel框架如何优雅,例如在第6章<Laravel框架中的设计模式>中写这个管道处理使用了装饰器模式,很elegant;在第7章<请求到响应的生命周期>又把装饰器模式提一遍,说中间件&请求是怎么用这个管道处理的。

书写得没错,但我读完之后却有种“马后炮”的感觉,因为我觉得书里面没有阐述明白这个Pipeline解决了什么问题?底层的代码总是为解决某一类问题而设计出来的。

"洋葱"模型是什么?

"洋葱"一词的来源

如果只读Laravel的使用文档,是没有关于洋葱模型的描述的。"洋葱"一词出自/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php

我们可以在  prepareDestination(Closure $destination) 看到这样的注释——

Get the final piece of the Closure onion. 译:获取闭包洋葱的最后一瓣。

在 carry() 看到这样的注释——

Get a Closure that represents a slice of the application onion. 译:获取应用洋葱的一瓣。(一瓣既一个闭包函数)

在Laravel框架中,很多注释和代码名称已经非常形象地表达了程序代码的功能,代码注释中将中间件称为“洋葱”层,将整个处理流程称为“管道”。

观察"洋葱"

 "洋葱"可以近似看作同心圆

在最外层的圆的任意一点向圆心画一条直线,这条直线会依次经过外层圆>内层圆>圆心>内层圆(对侧)>外层圆(对侧)

我们对这类型的业务需求用到的数据结构称之为洋葱模型。

计算机中使用了洋葱模型的设计

①js事件流(事件捕获与冒泡)

window事件捕获 > document事件捕获 > body事件捕获 > div事件捕获 > 事件处理 > 冒泡至div > 冒泡至body > 冒泡至document > 冒泡至window

②后端web框架中间件

中间件A前置操作 > 中间件B前置操作 > 请求处理 > 中间件B后置操作 > 中间件A后置操作

描述”洋葱“

”洋葱“毕竟是是个比喻,如果用计算机术语描述,我倾向于使用栈来表示。

以中间件为例

或者用一个特殊的数组(管道,特点:来回地遍历)这样表示

动手写一个洋葱模型

有了图之后,我们就可以动手敲代码啦

首先先造个洋葱

<?php
//php7.4

/**
 * 洋葱瓣
 */
interface Slice
{
    function pre_do(); //前置操作
    function suf_do(); //后置操作
}

/**
 * 洋葱
 */
class Onion
{
    //栈
    private array $stack = [];

    //入栈
    public function send(Slice $slice)
    {
        array_push($this->stack, $slice);
    }

    public function handle()
    {
        array_map(function ($slice) {
            $slice->pre_do();
        }, $this->stack);

        $reverseStack = array_reverse($this->stack);

        array_map(function ($slice) {
            $slice->suf_do();
        }, $reverseStack);
    }
}

然后我们来造些”中间件“ 看看效果

/**
 * 中间件A
 */
class MiddlewareA implements Slice
{
    public function pre_do()
    {
        var_dump("MiddlewareA 前置操作");
    }

    public function suf_do()
    {
        var_dump("MiddlewareA 后置操作");
    }
}

/**
 * 中间件B
 */
class MiddlewareB implements Slice
{
    public function pre_do()
    {
        var_dump("MiddlewareB 前置操作");
    }

    public function suf_do()
    {
        var_dump("MiddlewareB 后置操作");
    }
}


/**
 * 请求处理
 */
class HandleRequest implements Slice
{
    public function pre_do()
    {
        var_dump("处理请求");
    }

    public function suf_do()
    {
        //do nothing
    }
}

//主逻辑
$onion = new Onion();
$onion->send(new MiddlewareA);
$onion->send(new MiddlewareB);
$onion->send(new HandleRequest);

$onion->handle();

 执行结果如下

 


初步来说,实现需求了,但写得不优雅。那我们接下来一步一步优化我们的代码。


代码优化

何谓优雅?

优化代码的前提是知道哪里写得不好。

对比Laravel的Pipeline,我有如下启发

1.口语化(将英语中的实词给提取出来了)

我们来读一下下面这段代码

这段代码十分彰显面向对象功底。抽象得恰到好处,将用到的数据结构、相应的行为描述清楚了。

看着这段代码我们会很自然脑补出下述句子

 I'd like to send the request through the middlewares pipeline, then dispatch it to router.

将请求通过中间件管道,然后发送给路由器。

将XX通过XXX管道,然后XXX。

2.then()   圆心而不是圆形

前面我们提到了同心圆的例子,圆心是一个点,而不是圆形。给同心圆绘制任一直径所在的直线,我们会说“直线经过了圆心”不会说“直线先经过了圆心的左侧,再经过了圆心的右侧”这样怪异的表达方式

上文中自己写的洋葱模型的代码中,实现上有点投机取巧——将HandleRequest拆成了前置操作和后置操作并且使后置操作为空

代码执行顺序就会变成

MiddlewareA前置操作>MiddlewareB前置操作>处理请求前置操作>处理请求后置操作(空操作)>MiddlewareB后置操作>MiddlewareA后置操作

原本只有一步的”处理请求“就被拆成了两步

3.递归而不是遍历

我觉得这个点是可以快速提升代码逼格的。递归肯定比遍历要更耗费脑细胞,因为递归需要思考延续性,而且很多时候需要额外去思考递归的终点。

站在巨人的肩膀上构思

我们沿着Laravel给的“骨架” Illuminate\Contracts\Pipeline\Pipeline.php管道接口,结合上述几点思考来一步一步丰富这个“洋葱”模型。

遍历变成递归

数组的递归函数,我的第一反应是PHP的array_reduce()函数

这有什么难的嘛,不就是把用array_map()的地方变成array_reduce()嘛

麻溜的,我写出了下面的代码

    public function handle()
    {
//        array_map(function ($slice) {
//            $slice->pre_do();
//        }, $this->stack);
//
//        $reverseStack = array_reverse($this->stack);
//
//        array_map(function ($slice) {
//            $slice->suf_do();
//        }, $reverseStack);

        array_reduce($this->stack, function ($carry, $item) {
            $item->pre_do();
        });

        $reverseStack = array_reverse($this->stack);

        array_reduce($reverseStack, function ($carry, $item) {
            $item->suf_do();
        });
    }

真就换了个函数而已「汗」「汗」「汗」,可以说跟递归没有任何关系,闭包函数的$carry根本没用上啊!!!!

重新审视“洋葱”模型里的递归

递归两大要素:①延续性,既可以递推;②终止递归的条件

延续性:每一层洋葱瓣只需要关心这一层的前置操作是什么,后置操作是什么,不需要关心外层和内层洋葱瓣做了什么。每一层都实现Closure($passable)即可让$passable往下传递。

终止递归的条件:抵达“圆心”

于是就有了使用手册中的《基础功能>中间件》的handle定义

public function handle($request, Closure $next)
{
    // 前置操作
    $response = $next($request); //处理请求,生成响应
    // 后置操作

    return $response;
}

知秋君
上一篇 2024-07-17 22:12
下一篇 2024-07-17 21:48

相关推荐