停止使用花哨技巧編寫“優(yōu)雅”代碼!
在技術(shù)社區(qū)中,經(jīng)??梢钥吹揭恍┎┛屯茝V編程語言的各種“高級特性”和“高級模式”,并使用這些特性編寫一些“優(yōu)雅”的代碼。那么,學(xué)習(xí)和使用這些東西到底是好是壞呢?本文旨在幫助你做出判斷。
技巧的巧妙與智慧的智慧
許多人以使用語言和框架的晦澀特性為榮,通過利用各種罕見的 API 展示自己對框架的熟悉度。他們認為自己的編程和技術(shù)水平高于那些編寫簡單邏輯的學(xué)生。這個觀點是否合理?下面通過幾個例子來推導(dǎo)一下:
首先,在招聘開發(fā)人員時,有一個有趣的現(xiàn)象:層次越高的應(yīng)聘者,考察的具體編碼技能越少,而更多考察架構(gòu)能力、業(yè)務(wù)理解和工程質(zhì)量。請注意,理解架構(gòu)、業(yè)務(wù)和工程不是指溝通或管理等軟技能,而是工程師的扎實專業(yè)能力。
另一方面,在技術(shù)社區(qū)中,最受歡迎的內(nèi)容往往是各種框架和庫的“入門指南”。從暢銷的《XXX 從初學(xué)者到專家》指南到《一步一步教你如何學(xué)習(xí) XXX》,最熱門的內(nèi)容仍然圍繞如何操作這些 API 和應(yīng)用某些技術(shù)知識模式。
結(jié)合這些現(xiàn)象和一些常識,我們可以得出以下三個條件:
- 程序員的層次越高,其技能樹中技術(shù)知識的比例越小。
- 高級程序員的比例遠小于初級程序員。
- 在編程社區(qū)中,大多數(shù)人關(guān)心的是技術(shù)知識。
根據(jù)這些條件,我們可以做出一個松散的推論:那些最關(guān)注和癡迷于編程技能的人,很可能是初級程序員中的大多數(shù)。這樣,炫耀技巧不一定能表明技術(shù)水平高超。
需要明確的是,我們并不認為編程技能不重要。相反,高級程序員對技能的理解遠比初級程序員深刻,許多高超的代碼可以在數(shù)量級上優(yōu)化和解決問題。那么,我們應(yīng)該如何評價這樣的代碼呢?
能夠應(yīng)用各種高級特性的學(xué)生無疑被認為是“聰明”的。然而,何時何地使用它們需要基于所謂的“智慧”來判斷。這類似于 Facebook 和 Google 的代碼標(biāo)準中經(jīng)??吹降恼f法:
用你最好的判斷力。
雖然聽起來令人愉快,但這是一個非常玄妙的概念。下面我們將進行一些更具體的討論,并提取一些常見的技巧和技術(shù)。
常見的技巧和技術(shù)
使用危險的語義
許多人在閱讀了《高級程序設(shè)計》之類的書后,會將他們對高級特性的理解應(yīng)用到實際項目中,以炫耀自己的技術(shù)水平。在前端領(lǐng)域,這些行為包括但不限于:
- 了解 == 和 === 的區(qū)別,并在不同情況下使用不同的符號進行邏輯判斷。
- 了解變量提升行為,并利用它實現(xiàn)特殊的代碼執(zhí)行順序。
- 理解原型和構(gòu)造函數(shù),并使用它們實現(xiàn)各種繼承關(guān)系。
- 掌握 this 指向的各種規(guī)則,并利用特殊規(guī)則綁定上下文。
- ...
雖然使用這些特性的代碼確實可以運行,但問題在于這些語義都是危險的,或者是語言設(shè)計問題造成的糟粕。在已知它們難以使用并且有成熟替代解決方案的情況下,為什么要使用它們來炫耀自己的技術(shù)水平?然而在前端社區(qū)中,這種行為屢見不鮮。例如,僅僅理解 this
指針的各種規(guī)則就足以寫一篇長文(這在許多技術(shù)社區(qū)中早已成為一個乏味的話題)。而像 ==
這樣的特性雖然有無數(shù)的陷阱,令人驚訝的是仍有許多人在閱讀博客后“合理”地使用它們。至于變量提升,這種完全反直覺的設(shè)計缺陷被一些人用來創(chuàng)造各種花哨的面試題。
當(dāng)然,這絕不是反對理解這些所謂的“高級特性”如何工作以及為什么它們會導(dǎo)致混亂的行為。對于每一個想要成長的可靠學(xué)生來說,學(xué)習(xí)它們是很重要的。這里給出的建議是:
- 至少了解它們一次,達到能夠指出問題所在的水平。
- 學(xué)習(xí)這些特性的替代解決方案,并知道如何避免陷阱。
- 除非維護底層庫,否則不要在代碼中使用它們。
應(yīng)用設(shè)計模式
設(shè)計模式也是技術(shù)文章中非常常見的一個話題。例如,許多文章將《設(shè)計模式》中的幾十種模式應(yīng)用到 JavaScript 中,使用上述的各種“高級特性”模擬和實現(xiàn)這些模式。最后,他們會提升說這些模式都是“優(yōu)秀程序員必須知道的”,因此在簡歷上加上一行“熟練掌握各種設(shè)計模式”顯得很有吸引力!
設(shè)計模式的最初目的是彌補靜態(tài)語言如 Java 的不足。隨著編程語言的發(fā)展,許多“經(jīng)典”設(shè)計模式已經(jīng)成為語言機制的一部分。例如,export 對單例模式的內(nèi)置支持,用函數(shù)層包裹內(nèi)容就是工廠模式,yield 也實現(xiàn)了迭代器模式等等。此外,JS 的動態(tài)特性使得 JSON 的靈活性遠遠超過 reflection,而函數(shù)作為一等公民的設(shè)計使得 JS 的回調(diào)函數(shù)比 Java 的回調(diào)接口或 Visitor 模式靈活得多。
許多提倡設(shè)計模式的文章并不是因為它們?nèi)藶榈刂圃炝瞬槐匾膹?fù)雜性,而是因為它們創(chuàng)造了一種誤解,即“如果不使用 XX 模式,說明你的技能不足”。至少從個人閱讀優(yōu)秀開源項目源碼的經(jīng)驗來看,我沒有發(fā)現(xiàn)機械地應(yīng)用模式的實例;相反,問題得到了清晰的描述,隨后提供了可讀的抽象。當(dāng)然,可以事后在其中找到某些實現(xiàn)的模式;然而,我更愿意相信作者在編碼時并沒有“這里需要使用 XX 模式”的心態(tài)。然而,許多缺乏辨別能力的初學(xué)者可能會因為缺乏閱讀高質(zhì)量代碼的經(jīng)驗或受到公司遺留項目中舊代碼的影響,最終固執(zhí)地遵循這些刻板的方法。在我看來,這相當(dāng)遺憾。
減少代碼行數(shù)
我們都知道,通過復(fù)制粘貼生成的冗長和重復(fù)的代碼是不好的。然而,大多數(shù)復(fù)制粘貼發(fā)生在時間緊迫、沒有時間進行優(yōu)化的情況下??紤]到我們的同學(xué)們的工作強度,這可以理解。另一方面,還有另一種極端行為,即通過各種非常規(guī)手段“簡化”代碼,以實現(xiàn)“最簡潔”的代碼。
例如,剛開始學(xué)習(xí)函數(shù)式編程的學(xué)生可能會特別喜歡 a(b(c(d, e(f, g)))) 這樣的代碼,認為深度嵌套函數(shù)可以大大減少中間變量,從而節(jié)省代碼量;有些學(xué)生喜歡用邏輯運算符連接各種條件邏輯,并將它們?nèi)繉懺谝恍校?nbsp;a || b && c && d常見的還有實用函數(shù)的參數(shù)越來越多,直到一次性傳入一行。
再考慮一下,這樣的代碼是否增強了可讀性。深度嵌套的函數(shù)調(diào)用帶來了許多右括號,如 )))))), 這在 Lisp 中早已備受詬??;單行條件邏輯不利于調(diào)試;具有許多參數(shù)的函數(shù)往往表現(xiàn)復(fù)雜,難以調(diào)試。
這些編碼習(xí)慣可以很容易地以更好可讀性的形式替換,而不會帶來太多麻煩。然而,故意創(chuàng)建這樣的代碼可能會使后續(xù)的維護人員感到不適。對于行間斷和縮進的具體實踐,工具如 JavaScript Standard Style 可以自動處理大多數(shù)情況。
隱式重寫常識
現(xiàn)代工程框架通常提供許多可定制的接口,允許開發(fā)人員輕松修改框架的行為。例如,React 公開了上下文,而 Redux 和 MobX 等庫利用這個接口大大優(yōu)化了深度 prop 傳遞的體驗。然而,在典型業(yè)務(wù)代碼中,框架內(nèi)的許多隱含約定和規(guī)范,如果不合理地進行定制,會導(dǎo)致重大挑戰(zhàn)。這些類型的修改通常發(fā)生在不起眼的地方,但可能有很大的影響。
例如,在我們之前維護的一個項目中,有一個巧妙的修改,用自己的 XXX.BaseComponent 替換了 React.Component 基類。自定義組件沒有涉及任何與業(yè)務(wù)邏輯相關(guān)的更改,而是添加了一些無法解釋的初始化代碼。結(jié)果是關(guān)于 React 組件基類的隱含常識失效了。在維護時,乍一看替換的組件看起來很普通;然而,回滾會導(dǎo)致問題。此外,這些“黑魔法”代碼沒有注釋或文檔,不清楚最初引入它們是為了什么問題。對于這種編碼實踐,除了過于聰明之外,似乎沒有其他合理的評價。
另一個例子是這個項目中的另一個“聰明”做法,即根據(jù)請求路徑用三四個不同的自定義版本替換 window.fetch(不用擔(dān)心——不會告訴你為什么?。_@意味著當(dāng)維護人員編寫新的 fetch 請求時,不能依賴之前關(guān)于 fetch 的任何隱含知識,而必須通過追蹤前任的自定義版本進行調(diào)試——這很神奇,不是嗎?
還有一些隱式實踐,問題出在副作用上。例如,當(dāng)看到 user = getUser(id) 時,可能不會期望這個 getUser 函數(shù)不僅查詢用戶,還悄悄顯示提示消息、發(fā)送請求并清除當(dāng)前數(shù)據(jù)。當(dāng)然,在前端開發(fā)中,管理眾多與 UI 相關(guān)的網(wǎng)絡(luò)副作用本身增加了復(fù)雜性。然而,如果調(diào)用一個函數(shù)會導(dǎo)致許多連鎖結(jié)果,進一步增加復(fù)雜性,許多維護人員可能會選擇棄用和重寫。
重新發(fā)明輪子
在技術(shù)社區(qū)中,你經(jīng)??梢钥吹健白钊娴那岸藢嵱煤瘮?shù)”之類的合集,并且它們的點贊數(shù)往往很高。然而,一個 500 合 1 的小霸王游戲卡比超級馬里奧更有趣嗎?
我有幸讀過一些這樣的文章,發(fā)現(xiàn)這些打包的函數(shù)往往連固定主題都沒有:左邊是 getCookie,右邊是 deepClone,上面是 isEmail,下面是 scrollTop。每個實現(xiàn)只有幾行相當(dāng)于將英文函數(shù)名翻譯成中文的注釋,沒有測試用例、依賴配置或文檔。它們被稱為“小而美”。
這樣的代碼值得復(fù)制到你的項目中重復(fù)使用嗎?坦率地說,它們只是滿足“我可以發(fā)明輪子”沖動的產(chǎn)品。當(dāng)然,我完全相信作者可以輕松優(yōu)雅地編寫一個深拷貝函數(shù)。但是項目不是面試;對于一個穩(wěn)定可靠的輪子庫,除了簡單的實現(xiàn)外,還需要許多與代碼無關(guān)的東西。根據(jù)布魯克斯定律(《人月神話》)的軟件項目中,實際編碼時間只占 1/6;其余的大部分時間需要用于測試、文檔和溝通。對于具有更高質(zhì)量要求的庫代碼,匆忙編寫或從網(wǎng)上復(fù)制(哦不!讓我們稱之為內(nèi)聯(lián))代碼能否滿足?
在正式項目中使用庫,如果穩(wěn)定的現(xiàn)有依賴滿足需求,顯然應(yīng)首先選擇。如果遇到需要自己重新發(fā)明輪子的情況,確保在可靠項目的5/6
之外的時間也做好;不要不必要地重復(fù)劣質(zhì)輪子。
追求高層次的抽象
最后一點可能更為小眾,因為對于許多人來說,復(fù)制粘貼即可滿足需求,這違反了他們的習(xí)慣。然而,正因為如此,這是一種更高級形式的“巧妙”。
[高級] 聽起來像是純粹的圣杯。[高階函數(shù)] 和 [高階組件] 似乎是 [高級程序員] 的完美搭配。然而,如果你需要維護這些高階函數(shù),你會怎么想?
() => () => () => () => 123
一個返回 123 的函數(shù),由另一個函數(shù)返回,確實很高層次。但是這會不會讓你感到困惑...
結(jié)語
在編程中,技巧和智慧是不可或缺的,但僅僅依賴技巧并不能保證代碼質(zhì)量。通過理解和運用合適的設(shè)計模式、提高代碼可讀性以及避免不必要的復(fù)雜性,我們可以編寫出既高效又易于維護的代碼。這不僅是對技術(shù)能力的證明,也是對團隊合作和項目成功的承諾。