过度抽象
抽象是好的,这没错,能隐藏复杂实现背后的细节,而只将结果暴露出来——但这一切是有代价的。对于使用者来说,一般情况下我们希望达到的结果是方便和便捷,不需要理解背后的一切即可用一个调用解决问题。可一旦遇到问题,事实上仍然会逼迫使用者不得不去深入分析那些我希望隐藏的抽象。换言之,抽象就和幕布一样,我只是将其遮掩,但不代表它永远不被打开。我们希望的是它尽可能的不出问题,也就没有让人打开的动机,以及另外一点:如果真的要被打开,那我也希望它是一个「井井有条」而非「遍地垃圾」。我只是将复杂隐藏在幕后,而不是「把垃圾扫到看不见的地方」。
垃圾——我指的就是过度抽象。也许这么说有点过分,但这种抽象造成的损害就和垃圾一样,腐败发臭,让人恶心。过度抽象(或者说过早抽象)是一种为了优化而优化的做法,也是常常会发生在(像我这样)不上不下的中级工程师身上的事情。我自己也能很常常感受到,判断一部分代码是否需要抽象,的确是考验能力的地方。
对于是否需要抽象,我自己最直觉的判断方式是「次数」,简单粗暴,也很容易理解次数:多次出现意味着繁复,既然能够复用,那为什么不封装一下?大部分情况下都对,但不尽然全都如此。仔细反思这部分直觉,其实会很容易发现,当我关注「次数」的时候,我不是只看到它出现了多少次,而还有另一个东西是隐藏的,那就是「收益」。为什么我要封装?因为重复调用的代码块多次出现不仅不利于阅读,维护起来也很痛苦。是的,降低这种繁复是对我而言显而易见的收益。
因此,是否需要抽象就需要纳入除了「次数」之外的「收益」这一层维度。如果一段代码显著出现了多次——但是对它抽象没有显而易见的收益,我就需要考虑是否有必要封装了。比如这段重复出现了三五次,但逻辑复杂,对它抽象要浪费我一天时间,并且未来也根本不会用到。那我就没必要非这个功夫。这不是从「美观」而是从「投入产出」的角度上去考量的,作为工程师,我不是艺术家;我的目的是保证产品的稳健和可用,而不是美学价值。美妙的代码的确让人身心余元,但需要明确的成本权衡,否则就会变成「劳民伤财的样子工程」。
继续延展思考,就会发现这里还存在第三个隐藏的判断标准,当我思考到「未来会不会用到」这样一个明显的收益时,我在考虑的其实就是「稳定的功能」。就像扳手和锤子这样的工具,我对它的需求是稳定且持续的:需要经常使用,并且场景都差不多。代码也是一样,如果这段代码需要复用,且每次都是类似的需求和功能,对它进行抽象就是很自然而然的决定。否则需求多变,今天是A明天是B,今天要是装水管但明天要挖地道,那你就算封装了这个扳手,那也不见得能一直使用。这种情况下的抽象就是没意义的。
接着上面,可以继续延展——场景变化,但如果这些变化也是稳定的呢?比如虽然我公司的业务横跨装水管和挖地道这些,但都是「持续且稳定的」,那我就可以去开发多个工具。没错,这就是「边界」,抽象很好,但它不该是毫无边界的。我隐藏了什么?你不需要知道扳手跟锤子是怎么造的,只需要知道怎么用——职责清晰,用途明确。我暴露给你的是开箱即用的功能,而不是「用个锤子还得自己砍树造木柄」,对于后者来说,那就是不合格的抽象。因为就「是用锤子」这个场景来说,你没必要让用户自己安装木柄,如此复杂的调用是抽象不明确,该隐藏的复杂性没有隐藏的结果。
我们仍然可以继续扩展,假设我进行了一个抽象——继续用锤子吧——用起来很方便,再也不用自己拿石头去敲钉子了。这是一把合格锤子,能砸下钉子。就是稍微有点…太重了。哦,这是一个大铁锤,重达十公斤——等等,你搞毛呢?我敲个钉子而已,你给我这么大个玩意儿?没错,这就是我最后要说到的事儿,也就是「性能」。如果我只是为了解决一个小问题,最终的抽象解决了,却伴随着巨大的性能开支,那就需要权衡这是否值得。前面我们说到了「收益」,本质上后续的内容其实都可以算是对这一项的扩展。性能开支的是否合适显然要结合场景来考察,在高度敏感的场景下,即便你的抽象美观、合理且完全是教科书级别的设计模式运用,但却带来了不可接受的性能损失;那么,这个抽象仍然是一种「坏抽象」。就好像泳泳比赛,你从泳池一端跑到了另一端,哪怕速度突破世界纪录,也只会拿到无效分,还会被请出场外——不合时宜说的就是这种行为。
以上反过来操作就会成为「坏味道」:可重用次数不多、需求还在变化(除非有充分证据,否则不要去预判)、收益不确定、边界和职责不清晰,如此种种,都是「过度抽象」的信号。每次抽象之前都可以问问自己,我的抽象符合上述需要吗?我的场景之下,目前的抽象是收益明确的吗?不说能让自己得到资深工程师级别的优良成功,但至少可以避免塌陷,保证抽象在基本层面上是合格的。对中级工程师来说,不要求做到最好,能够稳定的产出合格成果,才是迈向资深的第一步。这就够了。