2.3 信任问题
顺序的人脑计划和回调驱动的异步 JavaScript 代码之间的不匹配只是回调问题的一部分。还有一些更深入的问题需要考虑。
让我们再次思考一下程序中把回调 continuation(也就是后半部分)的概念:
// A ajax( "..", function(..){ // C } ); // B
// A 和 // B 发生于现在 ,在 JavaScript 主程序的直接控制之下。而 // C 会延迟到将来 发生,并且是在第三方的控制下——在本例中就是函数 ajax(..) 。从根本上来说,这种控制的转移通常不会给程序带来很多问题。
但是,请不要被这个小概率迷惑而认为这种控制切换不是什么大问题。实际上,这是回调驱动设计最严重(也是最微妙)的问题。它以这样一个思路为中心:有时候 ajax(..) (也就是你交付回调 continuation 的第三方)不是你编写的代码,也不在你的直接控制下。多数情况下,它是某个第三方提供的工具。
我们把这称为控制反转 (inversion of control),也就是把自己程序一部分的执行控制交给某个第三方。在你的代码和第三方工具(一组你希望有人维护的东西)之间有一份并没有明确表达的契约。
2.3.1 五个回调的故事
可能现在还不能很明显地看出为什么这是一个大问题。让我构造一个有点夸张的场景来说明这种信任风险吧。
假设你是一名开发人员,为某个销售昂贵电视的网站建立商务结账系统。你已经做好了结账系统的各个界面。在最后一页,当用户点击“确定”就可以购买电视时,你需要调用
(假设由某个分析追踪公司提供的)第三方函数以便跟踪这个交易。
你注意到,可能是为了提高性能,他们提供了一个看似用于异步追踪的工具,这意味着你需要传入一个回调函数。在传入的这个 continuation 中,你需要提供向客户收费和展示感谢页面的最终代码。
代码可能是这样:
analytics.trackPurchase( purchaseData, function(){ chargeCreditCard(); displayThankyouPage(); } );
很简单,是不是?你写好代码,通过测试,一切正常,然后就进行产品部署。皆大欢喜!
六个月过去了,没有任何问题。你几乎已经忘了自己写过这么一段代码。某个上班之前的早晨,你像往常一样在咖啡馆里享用一杯拿铁。突然,你的老板惊慌失措地打电话过来,让你放下咖啡赶紧到办公室。
到了办公室,你得知你们的一位高级客户购买了一台电视,信用卡却被刷了五次,他很生气,这可以理解。客服已经道歉并启动了退款流程。但是,你的老板需要知道这样的事情为何会出现。“这种情况你没有测试过吗?!”
你甚至都不记得自己写过这段代码。但是,你得深入研究这些代码,并开始寻找问题产生的原因。
通过分析日志,你得出一个结论:唯一的解释就是那个分析工具出于某种原因把你的回调调用了五次而不是一次。他们的文档中完全没有提到这种情况。
沮丧的你联系他们的客服,而客服显然和你一样吃惊。他们保证,一定会向开发者提交此事,之后再给你回复。第二天,你收到一封很长的信,是解释他们的发现的,于是你立刻将其转发给你的老板。
显然,分析公司的开发者开发了一些实验性的代码,在某种情况下,会在五秒钟内每秒重试一次传入的回调函数,然后才会因超时而失败。他们从来没打算把这段代码提交到产品中,但不知道为什么却这样做了,他们很是尴尬,充满了歉意。他们以漫长的篇幅解释了他们是如何确定出错点的,并保证绝不会再发生同样的事故,等等。
然后呢?
你和老板讨论此事,他对这种状况却不怎么满意。他坚持认为,你不能再信任他们了(你们受到了伤害)。对此你也只能无奈接受,并且你需要找到某种方法来保护结账代码,保证不再出问题。
经过修补之后,你实现了像下面这样的简单临时代码,大家似乎也很满意:
var tracked = false; analytics.trackPurchase( purchaseData, function(){ if (!tracked) { tracked = true; chargeCreditCard(); displayThankyouPage(); } } );
经过第 1 章之后,这段代码对你来说应该很熟悉,因为这里我们其实就是创建了一个 latch 来处理对回调的多个并发调用。
但是,后来有一个 QA 工程师问道:“如果他们根本不调用这个回调怎么办?”哎呦!之前你们双方都没有想到这一点。
然后,你开始沿着这个兔子洞深挖下去,考虑着他们调用你的回调时所有可能的出错情况。这里粗略列出了你能想到的分析工具可能出错的情况:
· 调用回调过早(在追踪之前);
· 调用回调过晚(或没有调用);
· 调用回调的次数太少或太多(就像你遇到过的问题!);
· 没有把所需的环境 / 参数成功传给你的回调函数;
· 吞掉可能出现的错误或异常;
· ……
这感觉就像是一个麻烦列表,实际上它就是。你可能已经开始慢慢意识到,对于被传给你无法信任的工具的每个回调,你都将不得不创建大量的混乱逻辑。
现在你应该更加明白回调地狱是多像地狱了吧。
2.3.2 不只是别人的代码
有些人可能会质疑这件事情是否真像我声称的那么严重。可能你没有真正和第三方工具打过很多交道,如果并不是完全没有的话。可能你使用的是带版本的 API 或者自托管的库,所以其行为不会在你不知道的情况下被改变。
请思考这一点:你能够真正信任理论上(在自己的代码库中)你可以控制的工具吗?
不妨这样考虑:多数人都同意,至少在某种程度上我们应该在内部函数中构建一些防御性的输入参数检查,以便减少或阻止无法预料的问题。
过分信任输入:
function addNumbers(x,y) { // +是可以重载的,通过类型转换,也可以是字符串连接 // 所以根据传入参数的不同,这个运算并不是严格安全的 return x + y; } addNumbers( 21, 21 ); // 42 addNumbers( 21, "21" ); // "2121"
针对不信任输入的防御性代码:
function addNumbers(x,y) { // 确保输入为数字 if (typeof x != "number" || typeof y != "number") { throw Error( "Bad parameters" ); } // 如果到达这里,可以通过+安全的进行数字相加 return x + y; } addNumbers( 21, 21 ); // 42 addNumbers( 21, "21" ); // Error: "Bad parameters"
依旧安全但更友好一些的:
function addNumbers(x,y) { // 确保输入为数字 x = Number( x ); y = Number( y ); // +安全进行数字相加 return x + y; } addNumbers( 21, 21 ); // 42 addNumbers( 21, "21" ); // 42
不管你怎么做,这种类型的检查 / 规范化的过程对于函数输入是很常见的,即使是对于理论上完全可以信任的代码。大体上说,这等价于那条地缘政治原则:“信任,但要核实。”
所以,据此是不是可以推断出,对于异步函数回调的组成,我们应该要做同样的事情,而不只是针对外部代码,甚至是我们知道在我们自己控制下的代码?当然应该。
但是,回调并没有为我们提供任何东西来支持这一点。我们不得不自己构建全部的机制,而且通常为每个异步回调重复这样的工作最后都成了负担。
回调最大的问题是控制反转,它会导致信任链的完全断裂。
如果你的代码中使用了回调,尤其是但也不限于使用第三方工具,而且你还没有应用某种逻辑来解决所有这些控制反转导致的信任问题,那你的代码现在已经有了 bug,即使它们还没有给你造成损害。隐藏的 bug 也是 bug。
确实是地狱。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论