作者 | Isaac Lyman
譯者 | 崔皓
誰都喜歡可讀性強的代碼,希望接手的代碼容易閱讀,容易理解,從而減少交接的工作量,但并不是所有的代碼都有好的易讀性,接手前輩的“屎山”通常是一件令開發(fā)者非常痛苦的事情。
關(guān)于代碼有一種流行說法:代碼被閱讀的次數(shù)是它被書寫次數(shù)的十倍,而且產(chǎn)品的壽命越長,這個比例就越高??紤]到這點,我們似乎對“理解代碼”的投資明顯不足。開發(fā)者通常更側(cè)重于編碼的能力,而不是閱讀和解釋已有代碼的能力,即便這種場景在日常工作中會頻繁出現(xiàn)。
開發(fā)任務的前80-95%時間應該用來閱讀代碼以及文檔。在研究現(xiàn)有代碼的過程中,你可能會學到很多東西,只有讀完代碼之后才能說:“這個功能已經(jīng)存在了,或者是加入這個功能弊大于利”。
本文將為你介紹一些實用的代碼閱讀策略,你可以根據(jù)實際情況使用它們。
一、重構(gòu)局部變量和方法
有時候,一段代碼非常模糊會誤導讀者甚至讓人難以推理出其含義。一個幾乎沒有風險的方法是重新命名局部變量和私有方法,以更準確地描述它們的作用。這些類型的修改不會影響到當前工作文件之外的功能,只要注意避免命名沖突,就不會導致邏輯錯誤。如果可能的話,使用IDE的重構(gòu)工具(而不是文本查找和替換),這樣就可以一鍵重命名所有被使用的東西了。
例如,考慮下面這段JavaScript代碼
它閱讀起來非常困難,方法名ib對理解函數(shù)功能毫無用處。不過,這并不妨礙你對它做出推斷:
由于reduce是在a上被調(diào)用的(并且它返回到一個空數(shù)組),a應該是一個數(shù)組類型。
回調(diào)參數(shù)i將是該數(shù)組的一個元素。
reduce的第二個參數(shù),一個空的對象{},告訴我們回調(diào)參數(shù)o是一個字典(對象)。
所以,通過重命名,我們可以得到如下結(jié)果:
通過上面的調(diào)整,可以看到fn是把數(shù)組元素變成字典的鍵。這就揭示了函數(shù)ib的目的:將數(shù)組轉(zhuǎn)化為字典,用一個自定義的回調(diào)來確定索引每個元素的鍵。你可以把fn改名為getKey,而ib應該被命名為indexBy。重新命名一些標識符有助于我們理解代碼,而不需要改變它的邏輯,也不需要一下子考慮所有的部分。如果可以的話,強烈推薦修改。畢竟這樣可以提高代碼的可讀性,將使整個團隊受益,同時它并沒有增加或改變程序的功能。
二、搞清楚代碼是如何被調(diào)用的
大多數(shù)代碼都會被其他代碼調(diào)用。如果你在糾結(jié)一段代碼,那么搞清楚它的調(diào)用情況對于了解它的功能有非常大的幫助。可以將方法重命名為ThisBreaksOnPurpose。然后進行編譯,盡管在通過反射訪問的情況下,你在運行時才會看到錯誤,但編譯的錯誤提示會告訴這個方法在哪里被使用。
如果以上方法不可行,你可以通過文本搜索方法名。如果你很幸運,這個方法的名字在代碼庫中是唯一的。如果不是這樣,你可能會得到一個更大的結(jié)果集,并且不得不翻閱大量不相關(guān)的代碼。
三、搜索類似的代碼
有時,即使所有的標識符都被很好地命名,用例也很清晰,但是代碼還是很難理解。不是所有的代碼都符合編碼習慣。有時某個特定的操作并沒有遵循編碼習慣。在最壞的情況下,有問題的代碼出現(xiàn)在工作的代碼庫中,同時也沒有使用明顯的慣用語。
然而真正獨特的代碼在長期存在的代碼庫中是很少見的,特別是在單個表達式或代碼行上。如果花幾分鐘時間在項目中搜索類似的代碼,你可能會發(fā)現(xiàn)一些蛛絲馬跡來解開整個謎題。
全文搜索是其中最簡單的方法。你可以選擇一個突出的代碼片段進行搜索,搜索工具通常包括一個 "全詞 "搜索選項,這意味著搜索care.exe不會返回scare.exertion這樣的結(jié)果。如果你想進一步縮小范圍,可以用正則表達式而不是文本短語進行搜索。
當然,偶爾即使是正則表達式也不足以縮小范圍,沒有人愿意花幾個小時在搜索結(jié)果中尋找可能沒有幫助的東西。學習一些高級搜索技術(shù)也是值得的。許多程序員喜歡使用Unix的命令行工具,如grep和awk,或者在Windows上使用手寫的PowerShell腳本。我的首選是JS Powered Search,這是一個VS Code擴展,可以讓你在JavaScript中定義一個邏輯搜索查詢。
四、運行單元測試
在一個完美的代碼庫中,你可以通過使用單元測試了解代碼運行的狀態(tài)。但是大多數(shù)代碼庫并不完美;由于效率的原因,單元測試工作往往顯得可有可無,有時單元測試所描述的是過時的行為。盡管如此,檢查并執(zhí)行代碼測試仍舊是一個好主意。至少,他們會描述代碼的輸入和輸出。
如果沒有單元測試或者單元測試不夠全面,你還有第二次挽救的機會??梢跃帉懸粌蓚€測試來證明代碼是否存在的問題。如果發(fā)現(xiàn)問題并修復它然后提交修改,增加代碼庫的穩(wěn)定性,讓這段代碼具有自解釋的能力。你永遠不必擔心增加自動化測試會破壞現(xiàn)有的功能。
測試需要花費時間來編寫,但此舉可以大大提升代碼執(zhí)行效率。測試是代碼正常工作的實際證據(jù),有單元測試在你就會相信代碼功能不會被破壞。
五、使用Debugger工具
一旦有了單元測試,就有了很好的機制幫助你進行逐步的調(diào)試。設(shè)置一個斷點或在這段代碼的頂部添加一個斷點/調(diào)試器語句。然后運行測試。一旦碰到了斷點,執(zhí)行就會暫停,你可以每次前進一行,進入和退出函數(shù),并檢查范圍內(nèi)所有變量的值。
如果你知道哪些用戶行為觸發(fā)了相關(guān)的代碼,你就可以設(shè)置斷點并正常運行程序,與程序的界面進行交互。如果你這樣做,反饋回路會更長,但它也會使用更真實的數(shù)據(jù),這可能有助于你發(fā)現(xiàn)空引用和邊緣案例。
從上到下的逐行調(diào)試可能對運行幾十或幾百次的代碼不太有用,比如嵌套的循環(huán)。對于這樣的代碼,可以在每個循環(huán)中添加匯總的變量,方便在循環(huán)結(jié)束的時查看總量。許多集成開發(fā)環(huán)境還允許你設(shè)置條件性斷點,可以通過設(shè)置條件在循環(huán)中暫停并進入斷點從而查看對應變量的值。
六、搜索知識庫
如果你的團隊把編寫文檔作為開發(fā)過程的一部分,你可以快速跳過這一步。文檔不應該是唯一的真理來源,你應該依靠代碼來了解程序的行為方式。
文檔雖然可以解釋代碼的 "How",但它往往更擅長解釋 "Why"。有時你明白一段代碼在做什么,但從另一個角度看貌似有些不對。所以在改變它之前,你應該盡一切努力去了解原來的程序員是根據(jù)什么信息或約束來編碼的。
一篇好的內(nèi)部文檔也能為你指出知道真相的隊友。如果你已經(jīng)走到了這一步,做了足夠多的工作,那么可以向外尋求幫助。確保讓對方知道你在做什么工作,你想解決什么問題,他們很有可能會注意到你的視野盲區(qū)。
七、使用版本控制注釋
看到這里,你已經(jīng)了解了幾種有效的代碼閱讀策略。但即使如此,也可能會有無法解決的問題:一個奇怪的設(shè)計決定,一個打破代碼庫編碼模式的方法,一個沒有明顯理由的代碼特質(zhì)。
版本控制系統(tǒng)可以顯示代碼庫中任何一行代碼的作者和提交。在Git中,就是git blame命令。大多數(shù)系統(tǒng)稱它為"blue"或"annotate"。你可以在命令行或IDE中運行這個命令。出現(xiàn)的將是一個逐行的提交列表:一個提交哈希值,一個提交信息,以及一個作者。
如果該行代碼的最近一次提交沒有意義——例如它是一個格式化或空白的變化,就需要通過文件的變更歷史來找到引入該行代碼的提交。同樣,版本控制系統(tǒng)有一些工具可以幫助你做到這一點。
一旦你拿到了PR和Ticket,不僅擁有了代碼的背景,還可以找到與之相關(guān)的工作人員:代碼的作者、PR審核者、任何評論或更新Ticket的人、簽署QA的人。如果前幾種方法都不奏效,那么是時候該和前輩們聊聊了。
八、先理解,再編碼
通過對以上步驟的學習,或許對你有所幫助,特別是對代碼背景的理解以及功能的實現(xiàn)方面。在你繼續(xù)前進之前,還需要考慮重構(gòu)代碼以使其清晰,創(chuàng)建新的文檔,在這里投入的任何時間都會讓你和你的團隊在代碼的互動中獲得回報。
有效閱讀代碼的能力是一種秘密武器,它可以使你快速通過技術(shù)面試,并使你成為任何團隊的重要成員。擅長寫代碼的程序員是有價值的,擅長讀代碼的程序員就更具價值了。當生產(chǎn)中出現(xiàn)錯誤或急需開發(fā)新功能時,第一步也是最重要的一步就是理解,閱讀代碼是能讓你順利到達彼岸。
原文鏈接:https://stackoverflow.blog/2022/08/15/how-to-interrogate-unfamiliar-code/
譯者介紹
崔皓,51CTO社區(qū)編輯,資深架構(gòu)師,擁有18年的軟件開發(fā)和架構(gòu)經(jīng)驗,10年分布式架構(gòu)經(jīng)驗。