停止使用花哨的技巧來編寫“優雅”的代碼吧!
在技術社區經常看到一些博客推廣各種編程語言的“高級特性”和“高級模式”,并利用這些特性提供一些“優雅”的代碼。
但是這種花里胡哨的東西真的可以提升我們的編碼效能嗎?
恐怕 不是的!
炫耀技能并不一定表明技術水平高超
許多人以使用語言和框架的晦澀功能為榮,通過利用各種罕見的 API 來展示他們對框架的熟悉程度。
他們認為自己的編程和技術能力比那些寫簡單邏輯的同學要高。甚至還會產生 “傲慢” 的情緒。
這種觀點是 非常錯誤 的!
我們來看下面的例子:
在招聘開發人員時,存在一個有趣的現象,即:更高級別的申請人較少審查具體的編碼技能,而更多地審查架構能力、業務理解和工程質量。
請注意,理解架構、業務和工程并不是關于溝通或管理等軟技能,這是工程師堅實的專業能力。
另一方面,技術社區中最受歡迎的內容通常是各種框架和庫的“入門指南”。
從暢銷的《XXX從初學者到專家》指南到《一步一步教你如何學習XXX》,最熱門的內容仍然圍繞著如何操作這些API并應用某些技術知識模式。
考慮到這些現象和一些常識,我們可以得出以下三個條件:
- 程序員的級別越高,技能樹中技術知識所占的比例就越小。
- 高級程序員的比例遠小于初級程序員。
- 在編程界,大多數人關心的是技術知識。
根據這些條件,我們可以做出一個寬松的推論:最關心和癡迷于編程技能的人很可能是大多數處于初級水平的程序員。 因此,炫耀技能并不一定表明技術水平高超。
需要澄清的是,我們并不認為編程技能不重要。相反,高級程序員對技能的理解比初級程序員要深刻得多,許多熟練的代碼可以在數量級上優化和解決問題。
那么我們應該如何評估這樣的代碼呢?
能夠應用各種高級功能的學生無疑被認為是“聰明”的。然而,何時以及如何使用它們需要基于所謂“智慧”的判斷。
這與 Facebook
和 Google
的代碼標準中經常看到的說法類似:
運用你最好的判斷力。
雖然聽起來很悅耳,但這是一個形式上的概念。
下面我們將進行一些更具體的討論并提取一些常見的技巧。
常見技巧
幸福的家庭都是相似的;每個不幸的家庭都有自己的不幸。
——列夫·托爾斯泰
好的代碼就像好的文學作品,壞的代碼有它自己的壞處。
如果我們使用“聰明的技巧”來評估一段代碼,那么這段代碼的質量可能不是很好。對于使用各種技術的代碼,我們還可以找到許多這些技術應用不佳的場景來表明它們各自的問題。
使用危險的語義
很多人讀完《高級程序設計》之類的書后,都會將自己對高級功能的理解運用到實際項目中去炫耀。在前端領域,這些行為包括但不限于:
- 學習
==
和===
的區別,并在不同的情況下使用不同的符號進行邏輯判斷。 - 了解變量提升(
var
)行為后,使用它來實現特殊的代碼執行序列。 - 理解
prototype
和constructor
,并利用它們實現各種繼承關系。 - 掌握
this
指向的各種規則,并使用特殊的規則來綁定上下文。 - ……
使用具有這些功能的代碼當然可以運行,但這里的問題是這些語義都是 危險的,或者是語言設計問題造成的渣滓。
既然知道它們很難使用,并且有成熟的替代解決方案可用,為什么還要用它們來展示自己的技術技能呢? 然而在前端社區,這樣的行為卻屢屢發生。
例如:
- 僅了解各種規則
this指向
就足以寫一篇長文章(這在許多技術社區中一直是一個無聊的話題)。 - 令人驚訝的是,像
“==”
這樣有無數陷阱的功能,很多人在閱讀博客后竟然想要 “合理” 地使用它們。 - 至于變量提升,這種完全違反直覺的設計缺陷已被一些人用來創建各種花哨的面試問題。
當然,這決不妨礙理解這些所謂的“高級功能”如何工作以及為什么它們會導致令人困惑的行為。
對于每個想要成長的可靠學生來說,學習它們很重要。這里給出的建議是:
- 至少理解一次,達到能夠指出問題所在的程度。
- 了解這些功能的替代解決方案,并了解如何避免陷阱。
- 除非維護底層庫,否則在任何情況下都不要在代碼中使用它們。
每種編程語言在其開發過程中都不可避免地留下遺留問題。對于JS這樣一個一周誕生、向前兼容性要求非常高的語言來說,這個問題就更加嚴重了。
然而,隨著軟件工程的發展,這些設計缺陷帶來的危險語義已經慢慢淡入歷史。現在,像深入研究IE6兼容性問題一樣,深入學習、掌握和使用它們已經逐漸過時了。
應用設計模式
設計模式也是技術文章中非常常見的主題。
例如,很多文章都把《設計模式》中的幾十種模式應用到了 JavaScript
中,用上面提到的各種“高級特性”來模擬這個、實現那個。最后,他們還夸夸其談,這些模式都是“優秀程序員必知”,所以在簡歷上加上一句“精通各種設計模式”,就讓人印象深刻了!
設計模式的初衷是為了彌補Java
等靜態語言的缺點。隨著編程語言的發展,許多“經典”設計模式已經成為語言機制的一部分。
比如:export
內置對單例模式的支持,用函數層包裝內容就是工廠模式,yield
也實現了迭代器模式等等。
此外,JS
的動態性使得 JSON
的靈活性大幅超越了反射,而一等公民的函數設計使得 JS
回調函數比 Java
的回調接口或類似 Visitor
的模式更加靈活。
很多提倡設計模式的文章之所以沒有傳播開來,是因為它們人為地制造了不必要的復雜性,反而造成了一種誤解:“如果你不使用XX模式,就意味著你的技能缺乏”。
至少從我個人閱讀優秀的開源項目源代碼來看,我還沒有發現機械應用模式的實例;相反,需要解決的問題被清楚地描述,然后提供可讀的抽象。
當然,我們可以回顧性地識別其中的某些實施模式;然而,我更愿意相信作者并沒有以“這里我們需要使用XX模式”的心態來編碼。
許多缺乏經驗、缺乏洞察力的初學者可能會因為缺乏閱讀高質量代碼的經驗或受到公司遺留項目中舊代碼的影響而最終遵循嚴格規定的方法。在我看來,這是相當遺憾的。
減少代碼行數
我們都知道,通過復制粘貼生成的冗長且重復的代碼是不好的。
然而,大多數復制粘貼發生在期限緊迫且沒有時間進行優化的情況下。考慮到我們的工作強度,這是可以理解的。
另一方面,還有另一種極端行為,其目的是通過使用各種非常規手段來“簡化”代碼,從而達到“最簡潔”的代碼。
例如:
- 剛開始學習函數式編程的同學可能對這樣的代碼情有獨鐘
a(b(c(d, e(f, g))))
,認為函數的深度嵌套可以大大減少中間變量,從而節省代碼量; - 有些學生喜歡用邏輯運算符連接各種條件邏輯,并將它們全部寫在一行中,例如
a || b && c && d;
- 同樣常見的情況是,實用程序函數最終會寫入越來越多的參數,直到它們在一行中一次性傳遞。
我們再考慮一下這樣的代碼是否增強了可讀性:
- 深度嵌套的函數調用會帶來大量的右括號,例如:
))))))
,這在Lisp
中長期以來一直受到批評; - 單行條件邏輯不利于調試;
- 具有許多參數的函數往往表現復雜且難以調試。
這些編碼實踐可以輕松地替換為具有更好可讀性的表單,而無需太多麻煩。然而,故意創建這樣的代碼可能會讓后續的維護者感到不舒服。
對于有關換行和縮進的具體實踐,JavaScript
標準樣式等工具可以有效地自動處理大多數情況。
隱式重寫常識
現代工程框架通常提供許多可定制的接口,允許開發人員輕松修改框架的行為。
例如:React
公開了上下文,而 Redux
和 MobX
等庫則利用此接口來極大地優化深度prop
傳遞的體驗。
然而,框架內,存在許多隱含的約定和規范,當在典型業務代碼中進行不合理的定制時,可能會導致重大挑戰。這些類型的修改通常發生在不顯眼的地方,但可能會產生很大的影響。
例如:在我們之前維護的一個項目中,進行了巧妙的修改,React.Component
用基類自己的XXX.BaseComponent
行為替換了基類。
定制的組件并沒有涉及任何業務邏輯相關的改動,只是增加了一些莫名其妙的初始化代碼。
結果,有關 React 組件 Base 的隱含常識變得無效。維護時,乍一看,替換的組件似乎很普通。但是,修復就會導致問題。
此外,這些“黑魔法”代碼沒有評論或文檔,也不清楚為什么首先引入它們是為了解決什么問題。對于這樣的編碼做法,也許除了過于聰明之外,沒有其他合理的評價。
該項目的另一個例子涉及另一種“聰明”的做法:
window.fetch
根據請求路徑將其替換為三到四個不同的自定義版本。這意味著當維護人員編寫新的獲取請求時,他們不能依賴任何先前有關獲取的隱式知識,而是必須通過跟蹤前輩的自定義版本來進行調試!
還有一些隱性做法會因副作用而出現問題。例如:當看到 user = getUser(id)
時,人們可能不會想到這個getUser
函數不僅查詢用戶,而且還會默默地顯示一條提示消息發送請求,然后還清除當前數據。
當然,在前端開發本身中,管理大量與 UI 相關的網絡副作用本質上會增加復雜性。
然而,如果調用一個函數會導致許多級聯結果,導致復雜性進一步增加,許多維護者可能會選擇棄用和重寫。
重新發明輪子
在技術社區中,經常可以看到“最全的前端實用功能”之類的合集,而且點贊數往往很高。然而,500合1的小霸王游戲卡比超級馬里奧更好玩嗎?
我有幸讀過一些這樣的文章,發現這些封裝的函數往往甚至沒有固定的主題:左邊是“ getCookie
”,右邊是“ deepClone
”,上面是isEmail
,下面是scrollTop
。每個實現在將英文函數名翻譯成中文的層面上只有幾行注釋,沒有測試用例、依賴配置或文檔。它們被稱為“小而美”。
這樣的代碼值得復制到你的項目中以供重用嗎?坦白說,它們只是滿足“我能發明輪子”沖動的產品。
當然,我完全相信作者可以毫不費力地寫出優雅的內容。但項目不是采訪;而是項目。對于一個穩定可靠的庫,除了簡單的實現之外,還需要很多與代碼無關的東西。
根據《人月神話》中的布魯克斯定律,軟件項目中實際編碼時間只占1/6;剩余的大部分時間用于測試、文檔和溝通。對于具有更高質量要求的庫代碼——從在線源匆忙編寫或復制(哦不!讓我們稱之為內聯)代碼是否足夠?
在正式項目中使用庫時,如果穩定的現有依賴項能夠令人滿意地滿足要求,那么顯然這應該是您的首選。如果你遇到需要自己重新發明輪子的情況,那么請確保在可靠項目留下的 5/6 之外編寫代碼也做得很好;不要不必要地重復劣質輪子。
追求高級抽象
最后一點可能更小眾,因為對于許多僅通過復制和粘貼即可滿足需求的人來說,這違背了他們的習慣。但也正因為如此,它才是“詭計”的更高級形式。
“高級” 這聽起來像是一個純粹的名詞。
【高階函數】和【高階組件】似乎是【高級程序員】的絕配。
但是,如果需要維護這樣的高階函數,你怎么想?
() => () => () => () => 123
一個函數返回123,一個函數返回,另一個函數返回,確實是非常高級的。但這不會讓你感到困惑嗎……