发布于 2026-01-06 0 阅读
0

控制反转的反转

控制反转的反转

控制反转是扩展框架时经常遇到的现象。事实上,它通常被视为框架的一个决定性特征。——马丁·福勒

关键在于“框架”调用你,而不是你调用“框架”。这里的“框架”不必是任何特定的东西。操作系统、语言运行时或应用程序框架都可以是“框架”。当你注册事件处理程序时(无论是显式地使用 `.addEventHandler` 还是隐式地使用生命周期),你就已经在使用控制流 (IoC) 了。

但是 IoC 总是好的吗?或者我们应该说,IoC 总是我们想要的吗?

我认为大多数控制反转(IoC)并非有意为之(目前我能想到的只有依赖注入),但控制权本质上是反转的,尤其是在人机交互编程中,因为计算机的工作方式就是如此:用户控制一切。如果用户不输入,程序就会一直处于空闲状态。

# The example provided in the blog above 
puts 'What is your name?'
name = gets
process_name(name)
puts 'What is your quest?'
quest = gets
process_quest(quest)
Enter fullscreen mode Exit fullscreen mode

那么问题来了:为什么命令行查询似乎能保持控制权?线程难道不会阻塞并等待信号吗?的确如此,事实上,计算机在某种程度上是事件驱动的,以中断的形式出现。然而,从概念上讲,它仍然保持控制权,因为它是一个命令式过程:语句按顺序执行。控制流清晰可见。

我觉得有时候我们需要重新掌控局面,比如遇到回调地狱的时候。好在协程(async/await)可以帮我们解决这个问题。

// in control

async function foo() {
  const x = await fetch('http://bar.com/');
  //...
}

// control inverted

function foo() {
  fetch('http://bar.com/').then(x=>{
    //...
  })
  //...
  // we get two flows of control now?
}
Enter fullscreen mode Exit fullscreen mode

也许这个例子太简单了,你看不出任何区别 :/. 那我们来看另一个例子:你要管理一个 WebSocket 连接,它有一些规则:

  1. 连接成功后,服务器会向您发送“LOGIN”,您应该回复“LOGIN:<授权令牌>”,最后当服务器回复“VERIFIED”时,连接即成功。
  2. 当服务器发送“PING”时,客户端必须回复“PONG”(可能需要30秒),否则连接将被关闭。

通常情况下,WebSocket 是事件驱动的,我们可以编写类似这样的版本:

const socket = new WebSocket('foo.bar');
let isVerified = false;
socket.onopen = ()=> {
  socket.onmessage = ({data}) => {
    if(data=='LOGIN') {
      socket.send('LOGIN:'+authToken);
    }
    else if(data=='VERIFIED') {
      isVerified = true;
    }
    else if(data=='PING') {
      socket.send('PONG')
    } else {
      // process the data
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这段代码或许可行。但存在一些特殊情况,例如服务器没有返回预期消息怎么办?添加更多状态(例如 isLogined、isSuccessful)是一种解决方案,但这显得冗余(每次回调执行时都可以检查当前状态),而且如果步骤更多,则不易扩展。

然而,如果控制权反转,逻辑就会变得更加自然。假设我们有一个 A IoCWebSocket,它提供了一个WebSocket带有额外方法的修改版:

// resolve when it's open
async ready():Promise<void>;

// resolve when there is a incoming message
// reject when websocket error
async read():Promise<string>;
Enter fullscreen mode Exit fullscreen mode

那么逻辑就变成了:

const socket = new IoCWebSocket('foo.bar');
// ...
// assume inside an `async` function body
await socket.ready();
let next = await socket.read();
if(next!='LOGIN') {
  throw Error('Unexpected reply '+next);
}
socket.send('LOGIN:'+authToken);
next = await socket.read();
if(next!='VERIFIED') {
  throw Error('Unexpected reply '+next)
}
while(true) {
  try {
    next = await socket.read();
    if(next=='PING') {
      socket.send('PONG')
    }
    else {
      // process the data
    }
  }
  catch {
    // may catch error and re-throw if it's not due to connection closed.
    break;
  }
}
Enter fullscreen mode Exit fullscreen mode

你有没有注意到,这个顺序(这是一条不成文的规则)自然而然地得到了保证?

我们可能会遇到许多类似的情况,例如拖放、逐步查询、作弊码、(有状态的)动画和交互(按住激活/点击 n 次)……它们都是带有额外(临时)上下文信息的过程,它们的设计是为了遵循一定的顺序,它们可以拥有自己的控制流。

附加内容:协程作为状态机的替代方案

这篇文章主要讨论“是什么”。至于“如何做”,我仍在研究最优方案。Promise在大多数情况下,使用 async/await 就足够了。相关帖子:Promise!==协程

文章来源:https://dev.to/3shain/inversion-of-inversion-of-control-212p