恶劣的OO"中间件",与迷惑的事件驱动
发布于 6个月前 作者 tulayang 1050 次浏览

最近忽然想到了一些事情,把之前对connect模块印象中的“中间件”这个东西,“整改”了过来。当时做代码,懒得去细分,用了些connect所套用的“中间件”方式。“中间件”,java的东西。我对java的东西是越来越有难用的感觉。

因为套用中间件,让我没时间去思考事件驱动。在nodejs模块中,就目前来看,中间件主要是解决逻辑分发的问题。大白话,就是把一个处理任务下的多个逻辑,分放到多个文件中。next()函数在这里扮演了“拐点”的角色。

###next 有多么多余

next多少是“难看”和“多余”的。虽然一直用着不舒服,不过一直没时间多想。这几天,偏偏无聊,就想到了。对于js来讲,尤其nodejs,是有很好的分发血统的,那就是事件驱动。

在dom里,书写分发事件是一个家常便饭

elem1.addEventListener('click', fn1, false);
elem1.addEventListener('click', fn2, false);
elem1.addEventListener('click', fn3, false);
...

换算成多余的中间件就可以是这样

elem1.click(function() {// ...
    next();
}, false);
elem1.click(function() {// ...
    next();
}, false);
elem1.click(function() {// ...
    next();
}, false);
...

这代码真心多余到爆了!!! java本就是个擅长“多余”的语言!!! 当然,java程序员也许会叫ta“命令模式”!!!

###next 有多么违反抽象

function () { ... next() }
function () { ... next() }
function () { ... next() }
...

next永远只告诉你,下面运行一大片代码,却永远无法准确的告诉你下面要做什么事情。有时候,你不得不怀疑,你是不是在写一大堆js版本的goto!!! 当代码变得庞大,这会造成可维护问题以及扩展问题。设计模式的核心是抽象,怎么做的毫不重要,做什么才是最重要的。

parse();
print();
logger();

###如何修改next

  1. 扔掉中间件,如果可以的话扔掉面向对象,使用FP。如C语言所说,程序的本质,只有数据结构和算法。
  2. 如果要分发,使用事件驱动 要编写自己的事件模型,只需要继承内核的events.EventEmitter,通过on挂载、emit触发。
function connect (req, res) {
    res.on('parse', function () { // ...
        res.emit('print');
    });

    res.on('print', function () { // ...
        res.emit('logger');
    });
   
    res.on('logger', function () { // ...
        res.emit('...');
    });

    ...
}
20 回复

不是很明白你的意思,Connect是用来组织通用处理管道的,next就是进入管道的下一级,作为开发者当然应该清楚自己的整个管道的每一级是什么了

此外Connect的中间件跟Java的不是一个概念,只是名字刚好相同而已

这是一个很典型的connect中间件方式

function connect (/*f1, f2, ...*/) {
    var middlewares = [];
    for (var i = 0, len = arguments.length; i < len; i++) {
        if(typeof arguments[i] === 'function') {
            middlewares.push(arguments[i]);
        }
    }
    return function (req, res) {
        var self = this,
            args = arguments,
            i = 0;
        /*
        middlewares.forEach(function (middleware) {
            middleware.apply(self, args);
        });
        */

        function next () { 
            var middleware = middlewares[i],
                nextArgs;
            if(typeof middleware === 'function') {
                nextArgs = Array.prototype.slice.call(arguments);
                nextArgs.push(next);
                i++;
                middleware.apply(self, nextArgs);
            }
        }
        next.apply(self, args);
    };
}

@ravenwang

这就是违反了抽象、职责单一的原则。

一个函数,应该是完成工作的。看一下next方式的函数,都做了些什么:

function print () {
    // ...
   if (...) {...}
   else { next() }
}

有许多时候,为了能实现跳转,你不得不在函数中写逻辑。 而这些逻辑恰恰是摧毁函数可用性的源头。 这很像在写goto,而我们知道c语言是严禁goto的。

function print () {
    // ...
   if (...) {...}
   else { goto:log; }
}

print()的任务就是打印,不应该包含判断输出是否正确,下一个目标是不是A。

当你的代码容量膨胀到一定程度,你想在next这种组件上扩充的时候,你就要非常小心了。而且,当你拿到一些next代码的时候,你要理解作者的意图,恐怕是非常困难的。

@ravenwang

connect只不过是在模型上用了些函数式的东西,而不是完全的OO组合模式,但是他们的本质是一样的。而且connect的函数式风格并不纯粹,他只在组织函数栈上使用了函数式。

是的,顶一个,NodeJS就要用NodeJS的思维来写才好!

@tulayang

if (...) {...}
else { next() }
if (...) {...}
else { emit('log') }

这有多大区别?用emit还得确切地指明下一步的操作,把两个本应相互独立的操作耦合起来了

@tulayang 我想请教下什么是纯粹的函数式风格?

思路,观点很好啊。一个函数只做一件事。就如作者说的一样,print函数就只负责打印,不要搞大的逻辑分支。 但是,next并不一定只应用于这一场景啊。如:

function print(data) {
    console.log(data);
    next();
}

我觉得这样使用next也是一种不错的编程思想。做完了print,就做下一个处理(可以是log)。 所以,我觉得代码用在好的地方才能体现它的思想价值,用在不好的地方(就如作者举的例子),就显得多余。

@ravenwang

区别大了.

作为print(callback)这个函数,是原子级别的抽象。 对于next, 你将需要自己建立栈保存函数组,

function a (next) { print(next); } 请问你能告诉我控制器a打算干什么吗?

res.on('a', function () { prtint(emit('b')); })


我们把内容放大一下:

exports.index = c([
    function (next) { parse(next); },
    function (next) { print(next); },
    function (next) { logger(next); },
]);

或者

exports.index = c([
    parse(next),
    print(next),
    logger(next),
]);
exports.index = function (req, res) {
    res.on('parse', parse(emit(res, 'print')));
    res.on('print', function () { console.log('Hellow world!') });
    res.on('print', function () { ... test(...); ... });
    res.on('logger', logger(emit(res, '...')));
    ...
    res.on('print', print(emit(res, 'logger')));
};

哪一种具备可读和易分发,我想一目了然

@ravenwang

使用数据结构和算法来表示逻辑,而不是用对象。 使用独立的函数来封装逻辑,而不是用对象。

function Tree () { … } Tree.prototype.each = function (f) {}; Tree.prototype.next = function (f) {};

function each (tree, f) function next (tree, f)

下面的2个函数可以脱离关系,只要tree满足需要的结构。 上面的两个方法则必须要绑定在一起。

要扩展tree操作的时候,对于Tree,有时候需要考虑是否进行子对象的构建,以免Tree做了过多的事情。 而对于函数来讲,大家都是相互独立的,无需为其他外部因素考虑更多。

@tulayang 你是在用这种模式处理业务吗?我前面说了这种模式只适合处理管道,一有分支就不合适了,Connect用没问题,拿这种模式处理业务当然会觉得烂了,不然你拿你的事件驱动来处理Connect的场景,看得比Connect麻烦多少,不是这种模式恶劣而是用错了地方

@tulayang 对这个理解我不想做评论,风格没有好坏之分,只有适合不适合,goto也有适用的场景,追求「纯粹」的某种风格没有多大意义

@ravenwang

connect最大的问题就是抽象,这个语法糖并没有很好的解决模式面临的问题。

楼主是想要像promise的东西?

我怎么有种哭都哭不出来的感觉。。。 这是在说https://github.com/senchalabs/connect 这个Connect么? 怎么感觉说的和这个Connect一点关系没有,所谓的Connect的中间件,也就是一个Filter, 就像一根管子,它只知道流进到它的数据是什么,也只知道它流出去数据是什么,它不知道数据从哪里流进来的,也不知道流到哪里去(就完整性来说,可以不流下去也可以流到处理污水的管子(Error Handler),它就是根直管子。 你想想你手中有100只各种颜色的管子,自己把两头拼起来做成一根长管子,可以用xx根拼成yy色的管子,怎么组装完全是用户的事,可以用10根拼成单色的长管子,也可以用3根拼成3种颜色的管子。

Connect中间件本身,要么是业务独立的,要么是业务自我完结的,@tulayang 说了那么多,又要耦合又要各种分支跳转,这是在讲一个中间件的内部实现方式么?而且,Connect的中间件,可以是一个小方法也可以是一个完整的subapp,可能是1行也可能是XXX万行,和什么FP有哪怕一点点关系???

@zalemwoo 终于来了个明白人T.T

楼主这是滥用event,比next还要糟糕。 中间件的方式是有可改进的地方,比如许多中间件其实是提供额外的api,在支持trait的语言中可以用trait实现,这样性能也更好。但是就当前的javascript来说,Connect的方式已经是最好的了,就算用Koa也是next。因为中间件的模型本来就是管道。

当然,楼主可能是看到了一些写得有问题的中间件而产生想法。就像7楼说的,那是那个中间件有问题,干了不该中间件做的事情。其实connect的中间件很简单,是http中间件,也就是它应该只处理http层面的问题,任何用中间件直接做业务层面的事情都是有问题的。除非是7楼讲的是业务独立或自我完结的,或者是业务的这个部分可被直接映射到http层面上(比如authentication,可以直接用 http auth)。

回到顶部