并非抽象,而是中间层

129 阅读6分钟

如果你有过在某个软件系统中做重构/性能优化的工作经历,那你大概率遇到过这个典型的令人沮丧的场景:超级多抽象的代码。这些看起来是被整齐地组织好的、模块化的代码,其实往往是有着一层又一层的中间层的迷宫。他们性能拉垮,调试体验如在地狱一般,而且你的 CPU 貌似在这些抽象层代码上花了更多的时间,而没有在解决实际的问题。这让我们意识到:抽象之间,亦有差距。实际上,很多所谓的抽象,并不是抽象,他们只是一层薄薄的中间层,徒增复杂度,并没有带来实际的价值。

所以好的抽象是什么样子的?

好的抽象掩盖在其之下的复杂度。看看那些真正伟大的抽象,比如 TCP。TCP 帮助我们假装有一个可依赖的通信通道,即使它是建立在一个不可依赖的协议(IP) 之上的。它替我们承担了纠错、重传和包序列处理的复杂度,所以我们可以完全不管这些麻烦的事情。而且它把自己的工作完成得非常棒,以至于我们这些在上层的开发者们很少需要深究它的内部工作细节。你还记得上次在 TCP 网络包层面上对它进行调试是在什么时候吗?对于我们中的大部分人来说,答案是我们从没调试过。 这就是好抽象的标志。它允许我们在其上层工作,仿佛底层的复杂度完全不存在一样。我们能够利用抽象带来的服务,同时抽象帮我们屏蔽了困难复杂的底层细节,让我们眼不见,心不烦。

并非抽象

坏的抽象——或者更加精确些,伪装成抽象的中间层——是什么样子的?这些「抽象」不会隐藏任何的复杂度:他们加上的这一层的意义经常完全和它本应做的事情背离。想象一个在某个函数之上的「抽象层」,它不增加任何的行为,只是一个做跳转的层次。你肯定遇到过这样的「抽象层」——那些仅仅是在把数据从这传到那,让系统更难于追踪、调试和理解的类、方法或者是接口。他们并非抽象,他们只是中间层。

中间层的问题在于,他们会增加认知负担。中间层经常会被人们用灵活性和模块化这两个词标榜,但实际上很多中间层最后并没有带来这些好处。他们反而让代码库变得更加复杂、更加难以协作——特别是当我们需要修一个 bug,或是进行性能优化的时候。

抽象的代价

我们喜欢假装抽象是免费的。我们随意地增加又一个接口,又一个包装层。等到他们都多到能堆成个小山时,我们都还是不会意识到这一点。这样的思维方式忽视了一个基本的真相:抽象是有代价的。他们会增加复杂度,也往往会对性能有所损害。

抽象是性能的敌人。你增加了越多的层次,你就会离硬件越远。优化代码的过程成了扒开一层又一层直至抵达真正有效的工作的一场试炼。每一层都代表着心智和计算上的负担。我们要花更多的时间来明白代码,花更多的时间来找到真正重要的代码,同时硬件也在花费更多时间来执行真正的业务逻辑。

抽象也是简单的敌人。每个新的抽象都应该让事情变得更加简单——这难道不是进行抽象的初衷吗?但现实是,每一层都添加了它自己的规则、接口和失败的可能性。这些抽象并没有简化任何事情,而是层层对复杂度加码,让系统更加难以理解、维护和扩展。

所有的抽象都有缝

有名言曰:“所有的抽象都有缝。”这是真的。无论抽象有多么完美,最终,我们都会遇到需要理解底层细节的情况,想钻进缝中一探究竟。这些缝有时候是很隐晦的——像是你想了解性能状况(这里的复杂度是什么量级的?)——亦或有些缝是很显眼的,要求你深入地调试来搞明白为什么有些东西不按预期运行了。好的抽象会最小化这些情况的出现几率,而坏的抽象会让一个小 bug 变成一场对其的深入研究。

一条非常有用的评估抽象是好是坏的经验法则,就是问问你自己:我多久了解一次底层原理?一天一次?一个月一次?一年一次?需要打破幻象(仿佛抽象之下的底层细节不存在)的频率越少,抽象越好。

抽象代价的不对称性

抽象是有一定的不对称性的。抽象的作者马上就能享受抽象带来的好处——它让代码看起来更简洁、写起来更简单、更加优雅、可能也更灵活。但维护抽象的代价往往由他人承担:未来的开发者、维护者、和需要对付这些代码的性能工程师们。他们才是那些需要一层一层看代码、跟踪中间层、理解各种东西是如何配合工作的人。他们才是真正为不必要的抽象付出代价的人。

结论:聪明地使用抽象

这不是说抽象不好——远远不是。好的抽象是很强大的。他们允许我们构建复杂的系统,同时不会在高复杂度中迷失。但是我们必须认可,抽象不是免费的。他们有真正的在性能上和复杂度上的代价。如果一个「抽象」没有隐藏复杂度,而是简单的加了一个中间层,那么它就完全不是一个抽象。

下次你想创建一个抽象时,问问你自己:它真的在简化系统吗?还是它只是又一个中间层?聪明地使用抽象,并记住——如果你没有真正地隐藏复杂度,你就是在增加复杂度。

自译文,原文在此。