Snow Leopard 的修正准则

只要应用程序够复杂,每次操作系统升级都会弄坏几个本来工作得好好的功能,有些是应用程序本身代码的问题,只是在系统升级时才突然现身。这些 bug 在旧的操作系统上深藏不露,系统一升级才兴风作浪,大多因为操作系统的 API 行为变得更『正确』了。

升级到 Mac OS X Snow Leopard 之后我们的应用程序就出现了两个这样的 bug。第一个是应用程序 fork 之后立刻在子进程里调用 File Manager 的函数。跑了几年都没问题的代码,一到 Snow Leopard 上就僵死。第二个是 MPEG 的播放代码,同样是用户使用了很多年都没有问题,一到 Snow Leopard 上播放的视频就不停的闪烁。原因都是应用程序调用 API 的时机不对:File Manager 必须在 exec*() 函数调用之后才能使用;用 Core Video 配合 Quick Time 播放视频,必须在取出后一帧之后才能销毁前一帧。这些限制在 Leopard 版的文档上就有说明,幸运的不幸的是,系统『恰好』缓存了某些数据让不恰当的 API 调用『碰巧』能工作,掩饰了旧代码中的错误。显然,我们可以猜到,程序在 Snow Leopard 上才初次显露问题是因为新的操作系统去掉了那些缓存。

这样看来,OS X 岂不是自寻烦恼?旧的 API 行为在原来的应用程序里工作得好好的,何苦修改呢?大致猜想有这些原因:一是缓存让 API 的实现复杂化了。加上一个缓存,就得再加上一套代码保证缓存和真实数据的同步(或者在无法同步时清空缓存)。二来,因为缓存不能完全替代获取或者计算真实数据的代码,所以缓存、维护缓存同步的代码、计算真实数据的代码要同时占用内存。应用程序的活跃部分的体积会增大,原来能适合 CPU cache 的部分可能必须在主存和 cache 之间换进换出,本来为了提高效率的缓存却适得其反。最后一点,缓存让 API 行为变得不可预测。一段『碰巧』能工作的代码意味着当它碰巧不工作的时候更难被监测到和修正。

这些 bug,或者更广泛的说,所有系统升级中暴露的遗留代码的问题都说明了软件(特别是 API )设计的一个陷阱 —— 性能和 API 的确定性行为哪个更重要。结论是,确定性行为不仅更重要,而且往往带来更简洁的设计和更好的性能。

这是『修正准则』(Rule of Repair)的变形实例。Leopard 里的 API 不完美的修正了应用程序的错误输入(这里的输入是调用时机),引入了更复杂的实现和不确定的行为。Snow Leopard 去掉了缓存,消除了 silent success,但是 failure 不够 noisy。结果是功能被搞坏,新出现的 bug 诊断起来也很困难。所以,不要只在文档里说明你的 API 需要什么样的 precondition。如果在你的 API 文档里出现这样的文字 —— 在某某条件不满足的情况下调用某个 API 的行为是不确定的,那你就得三思。应用程序的作者不可能有时间细细品味文档,他们只会在出问题的时候(或者更窄的范围,程序崩溃或者挂起的时候;甚至没有人会注意你的 API 会有内存泄露,即使瞥一眼 Activity Monitor 就能看到疯狂的增长)才匆匆翻阅一下。所以,你的那些告诫会被人忽视数年。直到有一天你决定清理一下你的 API 实现,让它变得更简洁。这个时候那些告诫才会出来咬人。那么,亡羊补牢的方法是用一个 crash 来表达你的告诫(那样会在 debugger 里清晰的显露出出错点),而不是听任程序出现任何可能的行为(比如僵死和闪烁)。

不论如何,Snow Leopard 毕竟简化了实现,尽管做得不完美,起码比打着向后兼容的口号,为了让几个 killer-app 的 buggy code 正常工作而保留丑陋的实现然后把文档变成让人无法理解的技术诉讼书(或者辩护书)更好。

2条回应 to “Snow Leopard 的修正准则”

  1. fr3@K Says:

    第一次读这篇文字的时候我以为你是在说微软的视窗呢. 呵呵..

  2. fuzhou Says:

    不同公司对这个问题的回答都不会一样:究竟是放纵二次开发商
    随意解释行为,还是彼此约束求得统一。不同的商业模式往往导
    致不同的解释。

    曾经听过一个同事不无恼火地抱怨,有些所谓的fix就是发现了
    1+1 = 3就把它fix成1+1-1=3-1=2,理由是“我不知道别人是
    不是依赖这个错误的行为”,短期看好处当然是能让所有二次
    开发商happy,可这种哪里破了哪里糊的代码不停地发展下去,
    最后还有多少人愿意去猜测无数可能的行为,就难讲了。

留下评论