控制反转的反转
控制反转是扩展框架时经常遇到的现象。事实上,它通常被视为框架的一个决定性特征。——马丁·福勒
关键在于“框架”调用你,而不是你调用“框架”。这里的“框架”不必是任何特定的东西。操作系统、语言运行时或应用程序框架都可以是“框架”。当你注册事件处理程序时(无论是显式地使用 `.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)
那么问题来了:为什么命令行查询似乎能保持控制权?线程难道不会阻塞并等待信号吗?的确如此,事实上,计算机在某种程度上是事件驱动的,以中断的形式出现。然而,从概念上讲,它仍然保持控制权,因为它是一个命令式过程:语句按顺序执行。控制流清晰可见。
我觉得有时候我们需要重新掌控局面,比如遇到回调地狱的时候。好在协程(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?
}
也许这个例子太简单了,你看不出任何区别 :/. 那我们来看另一个例子:你要管理一个 WebSocket 连接,它有一些规则:
- 连接成功后,服务器会向您发送“LOGIN”,您应该回复“LOGIN:<授权令牌>”,最后当服务器回复“VERIFIED”时,连接即成功。
- 当服务器发送“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
}
}
}
这段代码或许可行。但存在一些特殊情况,例如服务器没有返回预期消息怎么办?添加更多状态(例如 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>;
那么逻辑就变成了:
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;
}
}
你有没有注意到,这个顺序(这是一条不成文的规则)自然而然地得到了保证?
我们可能会遇到许多类似的情况,例如拖放、逐步查询、作弊码、(有状态的)动画和交互(按住激活/点击 n 次)……它们都是带有额外(临时)上下文信息的过程,它们的设计是为了遵循一定的顺序,它们可以拥有自己的控制流。
附加内容:协程作为状态机的替代方案
这篇文章主要讨论“是什么”。至于“如何做”,我仍在研究最优方案。Promise在大多数情况下,使用 async/await 就足够了。相关帖子:Promise!==协程