愚蠢的 on('data', cb) 接口设计

愚蠢的 on('data', cb) 接口设计,在所有语言的 io 库中,但凡是类似这种设计的 API,都是十分愚蠢的设计。

正确的设计应该是类似 asio 中的 async_op(cb),其中 cb 只会被响应单次,除非在 cb 中重新发起 async_op(cb),当然也可发起其它异步操作。

因为 async_op(cb) 更为灵活,且包含了 on('data', cb) 这种用例。

另外由其当需要将这种异步方法转换成协程支持的时候,就能更加体现出它的优势所在了。

而不是在 on('data', cb) 中处理接收到的数据时,不停的判断接收到的数据是属于哪个流程,这既繁琐又易出错,这由其是在处理一些较复杂的应答式协议时更加糟糕无比,比如:

void onread(data) { // onread 就是 on('data', onread),接收到数据将被调用
  if (state == login) {
    on_process_login(data); // 当前为login状态,则按login逻辑处理接收到的data
    return;
  } else if (state == getwork) {
    on_process_getwork(data); // 当前为getwork状态,则按getwork逻辑处理接收到的data
    return; 
  } else if ... // 其它状态的判断...
}

上面这种模式十分糟糕,除了需要用户维护复杂的 state 状态之外,其请求发送部分代码被迫割裂在其它地方运行,而接收响应部分被统一到了一个 onread 函数当中,这是十分糟糕的。

正确的打开方式,通常像这种很多这种应答式协议流程应该用协程来处理,将会十分优雅:

void start_work() {
  await login(request);
  await read(data);
  // 处理server返回的data...然后继续后面的工作
  await getwork(request);
  await read(data);
  // 处理data,根据work完成业务
  ...
  // 继续使用协程以最自然的同步流程方式处理后续流程,而不需要 state 保存状态...
  ...
}

即便是那种简单的一问一答被动响应式的协议处理,在只处理请求的 server 端中的事件响应,也可以以下方式通过运行一个循环来异步读取就行了,而不必是 on('data', onread)

void work_loop() {
  while (true) {
    data = await read();
    if (data.type == request1) on_request1(data);
    if (data.type == request2) on_request2(data);
    ...
  }
}

综上可看出,on('data', onread) 这类接口十分难以使用在这种复杂的应答协程,也很难转换成协程接口(转换成协程这个工作不适合在用户代码中去做库本该做的工作),其次状态机的维持极其容易出错。

async_op 这种类型的接口则很容易转换成协程,因为 IO发起 与 IO完成,能轻松一一对应到协程的 suspendresume,即便不是协程也可亦或是 Resumable 函数,这也会得到一个较为清晰的业务流程。

简而言之,异步 io 库的 api 接口设计,不应该设计为 setInterval,而应是 setTimeout,但凡只设计为 setInterval 这种方式的 io 接口,都是十分糟糕的设计。

当然,与其说是 setIntervalsetTimeout 的区别,更应该说是事件驱动模型和异步操作模型的区别,一个库在实现IO接口时,应当尽可能设计能作为异步操作模型为基础的接口,或者直接设计为异步接口。

事件驱动模型在处理流程复杂的应答式协议时,需要维护状态机,使代码难以理解和维护;而异步操作模型则更加灵活,可以通过协程等方式将异步操作转换成同步代码,使代码结构更加清晰易懂。




Comments

blog comments powered by Disqus