過度抽象
抽象是好的,這沒錯,能隱藏複雜實現背後的細節,而只將結果暴露出來——但這一切是有代價的。對於使用者來説,一般情況下我們希望達到的結果是方便和便捷,不需要理解背後的一切即可用一個呼叫解決問題。可一旦遇到問題,事實上仍然會逼迫使用者不得不去深入分析那些我希望隱藏的抽象。換言之,抽象就和幕布一樣,我只是將其遮掩,但不代表它永遠不被開啟。我們希望的是它盡可能的不出問題,也就沒有讓人開啟的動機,以及另外一點:如果真的要被開啟,那我也希望它是一個「井井有條」而非「遍地垃圾」。我只是將複雜隱藏在幕後,而不是「把垃圾掃到看不見的地方」。
垃圾——我指的就是過度抽象。也許這麼説有點過分,但這種抽象造成的損害就和垃圾一樣,腐敗發臭,讓人惡心。過度抽象(或者説過早抽象)是一種為了最佳化而最佳化的做法,也是常常會發生在(像我這樣)不上不下的中級工程師身上的事情。我自己也能很常常感受到,判斷一部分程式碼是否需要抽象,的確是考驗能力的地方。
對於是否需要抽象,我自己最直覺的判斷方式是「次數」,簡單粗暴,也很容易理解次數:多次出現意味著繁複,既然能夠複用,那為什麼不封裝一下?大部分情況下都對,但不盡然全都如此。仔細反思這部分直覺,其實會很容易發現,當我關注「次數」的時候,我不是隻看到它出現了多少次,而還有另一個東西是隱藏的,那就是「收益」。為什麼我要封裝?因為重複呼叫的程式碼塊多次出現不僅不利於閱讀,維護起來也很痛苦。是的,降低這種繁複是對我而言顯而易見的收益。
因此,是否需要抽象就需要納入除了「次數」之外的「收益」這一層維度。如果一段程式碼顯著出現了多次——但是對它抽象沒有顯而易見的收益,我就需要考慮是否有必要封裝了。比如這段重複出現了三五次,但邏輯複雜,對它抽象要浪費我一天時間,並且未來也根本不會用到。那我就沒必要非這個功夫。這不是從「美觀」而是從「投入產出」的角度上去考量的,作為工程師,我不是藝術家;我的目的是保證產品的穩健和可用,而不是美學價值。美妙的程式碼的確讓人身心餘元,但需要明確的成本權衡,否則就會變成「勞民傷財的樣子工程」。
繼續延展思考,就會發現這裡還存在第三個隱藏的判斷標準,當我思考到「未來會不會用到」這樣一個明顯的收益時,我在考慮的其實就是「穩定的功能」。就像扳手和錘子這樣的工具,我對它的需求是穩定且持續的:需要經常使用,並且場景都差不多。程式碼也是一樣,如果這段程式碼需要複用,且每次都是類似的需求和功能,對它進行抽象就是很自然而然的決定。否則需求多變,今天是A明天是B,今天要是裝水管但明天要挖地道,那你就算封裝了這個扳手,那也不見得能一直使用。這種情況下的抽象就是沒意義的。
接著上面,可以繼續延展——場景變化,但如果這些變化也是穩定的呢?比如雖然我公司的業務橫跨裝水管和挖地道這些,但都是「持續且穩定的」,那我就可以去開發多個工具。沒錯,這就是「邊界」,抽象很好,但它不該是毫無邊界的。我隱藏了什麼?你不需要知道扳手跟錘子是怎麼造的,只需要知道怎麼用——職責清晰,用途明確。我暴露給你的是開箱即用的功能,而不是「用個錘子還得自己砍樹造木柄」,對於後者來說,那就是不合格的抽象。因為就「是用錘子」這個場景來說,你沒必要讓使用者自己安裝木柄,如此複雜的呼叫是抽象不明確,該隱藏的複雜性沒有隱藏的結果。
我們仍然可以繼續擴展,假設我進行了一個抽象——繼續用錘子吧——用起來很方便,再也不用自己拿石頭去敲釘子了。這是一把合格錘子,能砸下釘子。就是稍微有點…太重了。哦,這是一個大鐵錘,重達十公斤——等等,你搞毛呢?我敲個釘子而已,你給我這麼大個玩意兒?沒錯,這就是我最後要說到的事兒,也就是「效能」。如果我只是為了解決一個小問題,最終的抽象解決了,卻伴隨著巨大的效能開支,那就需要權衡這是否值得。前面我們說到了「收益」,本質上後續的內容其實都可以算是對這一項的擴展。效能開支的是否合適顯然要結合場景來考察,在高度敏感的場景下,即便你的抽象美觀、合理且完全是教科書級別的設計模式運用,但卻帶來了不可接受的效能損失;那麼,這個抽象仍然是一種「壞抽象」。就好像泳泳比賽,你從泳池一端跑到了另一端,哪怕速度突破世界紀錄,也只會拿到無效分,還會被請出場外——不合時宜說的就是這種行為。
以上反過來操作就會成為「壞味道」:可重用次數不多、需求還在變化(除非有充分證據,否則不要去預判)、收益不確定、邊界和職責不清晰,如此種種,都是「過度抽象」的訊號。每次抽象之前都可以問問自己,我的抽象符合上述需要嗎?我的場景之下,目前的抽象是收益明確的嗎?不說能讓自己得到資深工程師級別的優良成功,但至少可以避免塌陷,保證抽象在基本層面上是合格的。對中級工程師來說,不要求做到最好,能夠穩定的產出合格成果,才是邁向資深的第一步。這就夠了。