作為一名程序員,總有一些時候,會對自己所做的重復性的工作感到厭倦,也會羨慕明星項目做得熱火朝天 Star 數蹭蹭上漲。而讀代碼,則是緩解焦慮的良方。
每當讀懂軟件的精彩設計,贊嘆優(yōu)美整潔的代碼,甚至發(fā)現藏在注釋中的彩蛋時,都好像在不同的時空與作者產生了交叉,暢快地聊了會兒天。
讀代碼很有趣,但要讀通讀懂也很費功夫。本文是我在日常讀代碼中積累的一點心得,分享出來,希望能與大家產生共鳴。
1. 尋找一位好老師
優(yōu)秀的項目就像一位好老師,我們可以從它身上全方位地學到各種領域知識。
不過在開始讀代碼之前, 最大的問題就是:怎么樣才能找到合適的代碼項目?
Star 數高的項目更優(yōu)秀嗎?從某些角度講的確是的,但是在 gitstar-ranking 上, 4k 個 Star 的 Repo 只能排到第 5000 名,而至少有 50k 個 Star 才有可能排進前一百。光看 Star 數,這項目太多了根本選不出來啊。
在我看來,抓住如下幾個方向,一般就能篩選到合適的項目了:
興趣使然
首先就是一定要選自己感興趣領域的項目。
不少代碼片段都是比較枯燥而難以閱讀的(比如“飛一般”的位操作,為提升性能而莫名其妙的語句,或是包含了大量隱含知識等等),只有自己感興趣,才會有讀下去的意愿和動力,才能在其中發(fā)現樂趣。
有時候我們開始讀代碼的契機就單純只是在工作當中用到了,想要進一步了解其原理和設計,這是一個不錯的起點。想必很多同學第一次看源碼都是因為一層層追到了 Spring 的 Class 里面去了吧。
有的時候可能就是覺得某項技術很神奇,像魔法一樣,越是猜不透,就越想了解它是怎么“施法”的。
總之一旦有了興趣,就會很想進一步去了解它。不過,如果讀到一半又失去了興趣,也請大膽放棄它。失去了這一片草叢,還有整個樹林等著我們去探索。
經典且被大量使用
經典的、擁有大量用戶的項目,經歷了時間的考驗,不斷地迭代,通常在設計上都有很多出眾之處。
經典項目的維護者一般都是非常資深的工程師,并且也都會有大公司贊助,確保了代碼的高質量。這類項目在閱讀的過程中能學到很多知識,包括架構抽象、性能優(yōu)化、工程化等等。
比較常見的典型的項目有如:Go、Kubernetes、MySQL 等等。
合適的規(guī)模
代碼量太過龐大的項目,有時雖然很出名,但難免令人生畏。實際上可以找到很多行數不多,但依然精彩的代碼庫。
首先就是各種語言的標準庫,比如 Java 的 Stream、Lock 的實現等。另外也有不少開源的小而美項目,比如 redis、leveldb 等。甚至,許多經典大學課程里面的 Lab 也隱藏著優(yōu)秀的代碼,比如 xv6。
總之,這一類代碼可能幾天或幾周就能大致看完主干,特別適合學習設計思想。
2. 先看文檔
選定了項目,我們就差不多能對它有一些淺嘗輒止的了解了。
這時候,先不要直接 Clone 代碼。代碼完整地包含了所有知識,但也將細節(jié)毫無保留地暴露出來,直接進到代碼里面很容易迷失方向。
對于事物的理解,從全局到部分,從抽象到細節(jié)才是一個比較容易讓人接受的過程。
成熟的項目通常會有比較詳盡的文檔,文檔一般分為兩類:給用戶看的使用文檔,和給貢獻者看的開發(fā)文檔。
了解概覽
通過閱讀使用文檔我們能快速地了解到項目創(chuàng)立的目的、解決了哪些問題,以及從用戶的視角看該軟件是什么樣子的。除了看 overview,我也會大致關注配置,通過必填配置可以進一步了解軟件的依賴和外部特性。
以 TiDB 為例,它的使用文檔截圖如下:
從左側邊欄能了解到使用文檔的結構包括了簡介、部署、配置、參考等部分。這些部分都是使用者最關心的內容。
架構和模塊
優(yōu)秀的開發(fā)文檔一定會包括整個軟件的架構模型,和關鍵模塊的設計。
通過閱讀架構圖和高層設計,軟件的原理以及解決問題的思路一目了然。對于有一定經驗的讀者甚至可能看到架構設計后就已經大概知道軟件的工作流程了。
上圖是 TiDB 開發(fā)文檔截圖,我們發(fā)現它不僅包含了架構設計,還事無巨細的告訴讀者如何啟動代碼、怎樣貢獻、詳細的設計流程等等。除了架構設計,比較完善的開發(fā)文檔也會包含關鍵模塊的信息。關鍵模塊可能會涉及核心邏輯的設計和數據結構,以及邊界處的契約和交互方式等。對于 Go 語言這種持續(xù)演進的開源編程語言,甚至專門建立了一個 Proposal 倉庫來追蹤各種提案的設計、討論、代碼以及發(fā)布的情況,比如等了 10 年的泛型提案:
搞明白了架構模型和關鍵模塊,真正打開源碼時就能將包、文件名、接口等包含的知識與整體結構相互映射,在腦中形成一張完整的圖。
其他的前置知識
有時候文檔的作者還會加上不少前置知識,比如基于什么樣的算法,受到了哪些知識的啟發(fā)甚至是實現了哪篇論文的思想等等。
這些前置知識,對我們的理解會大有幫助。我們可以通過學習這些知識來進一步了解軟件的細節(jié)設計。
上圖是 etcd 的 github 頁面,在顯眼位置標明了它采用 Raft 共識算法,并鏈接到 Raft 算法的主頁,如果我們沒了解過 Raft,直接去讀 etcd 的代碼,很可能就對里面的選舉、日志復制等概念一知半解,這就好像在看沒有字幕的外文電影,精彩程度大打折扣。
3. 再讀代碼
看完了文檔,就可以開始看代碼了。為了防止在代碼中迷失方向,我們可以遵循幾條原則來閱讀:
從入口開始
雖說通過架構模型以及包和文件劃分的關系,我們能大致確定哪些代碼是核心代碼,但從入口處開始看會更符合大腦的思考方式。
因為入口代碼的工作一般是先對各種模塊進行初始化,然后調起主線程或者啟動主服務,這種明確順序的簡單工作讓我們不會一開始就遇到困難,循序漸進的過程更容易讓大腦產生獎勵。
如圖所示是 kubelet 啟動入口簡化后的主線邏輯,非常清晰。以此為起點沉下去,就可以分三路去細看配置詳情、創(chuàng)建 kubelet 的詳情,以及啟動的詳情。
抓住主線,從抽象到實現
主線就是從輸入是怎么樣一步步產生輸出的。在這一過程中,會涉及到多個模塊,每一個模塊又有自己的輸入和輸出。
當我們順著函數調用、數據傳輸方向一步步向下時,隨著抽象層次的不斷降低,涉及到越來越多的細節(jié),這個時候應該及時折返,不要一路看到底,很容易迷失在里面。
良好的設計會有合理的抽象,根據不同的開發(fā)語言,我們可以通過查看包、接口、特性、公有方法列表、頭文件等等來快速獲取抽象信息,逐步地拼接出程序主線。搞清楚了主線,再逐步將抽象展開,閱讀具體實現代碼。
仍以 kubelet 為例,kubelet 作為負責整個節(jié)點運轉的核心,工作多且雜。
但看它的代碼分包結構,非常清楚地將不同功能點劃分到不同目錄下,結合初始化邏輯,再進一步深入到每個功能目錄內,又可以發(fā)現 kubelet 的模塊設計遵循的是多個 manager 圍繞著一個核心共同協(xié)作的模型。好的抽象,就像一顆洋蔥一樣,層層分明。
一邊閱讀一邊記錄
初識一個項目,對結構和流程把握的不會太清楚,因此一邊讀一邊寫寫畫畫是很重要的。
有的時候跳轉次數比較多,前面看過的東西后面就忘記了,所以對關鍵路徑,記錄具體的函數名、模塊名,能幫助我們快速回溯到入口。
也有的時候遇到了需要拓展的知識盲區(qū),為了不打斷主線思路,可以先記錄下來,找其他時間再學習。
另外,遇到不直觀的、難以形成概念的代碼表達,翻來覆去的看也看不懂,這個時候就需要畫個圖來幫助理解了。
一個典型的例子就是在學習 B+Tree 的分裂、合并、上移下移的時候,全看代碼特別不直觀,想要理解這類內容畫圖定有奇效:
必要時借助 debug
有一些代碼為了正確性、性能等考慮,其表述可能會讓人百思不得其解。人類的思維方式是偏向順序的,用軟件開發(fā)做類比就是,我們更容易理解 Happy Path,而忽視分支細節(jié)。
當橫豎想不通某段代碼為什么要這么寫的時候,實際運行一遍,加斷點 Debug 一下可能就會發(fā)現真實的原因了。
一個有趣的例子是:在環(huán)形隊列中,判斷隊列是否為空需要看頭指針和尾指針是不是已經重合,下圖的代碼來自一個無鎖環(huán)形隊列的判空實現。
道理上講,環(huán)形隊列入隊 tail++,出隊 head++,先有入隊,才會有出隊,所以 tail 一定比 head 大。那為什么上面代碼里,除了判斷 tail - head == 0 以外,還一定要加上當 tail < head 時也認為空呢,這根本不可能發(fā)生啊?
實際的原因是,由于該環(huán)形隊列是無鎖的, tail 和 head 之間不保證任何同步,那么就可能由于調度因素,導致不同線程讀到不同時刻的值,結果 tail < head 就真的產生了。
想要搞清楚這種場景,最好的辦法就是真正執(zhí)行幾百萬次測試,通過條件斷點讓代碼在發(fā)生 tail < head 時停住,再觀察內存中的值來解釋。
4. 寫篇文章講講整個設計
代碼看個七七八八,差不多就對設計和實現都有一定的認識了。這時候心里多少會有點沖動想要把獲得的知識講出來。那么最好就是寫篇文章,寫文章可以對知識進行梳理,在寫的過程中也會不斷加深印象。隨著文章的撰寫,作者的設計意圖亦會越來越清晰,對軟件的理解也會越來越深刻。
整理大綱
寫文章,目錄最關鍵。一篇文章是不是有邏輯性,結構是不是清晰,全都在大綱的設計上。
既然我們的內容是講軟件的設計與實現,那么文章的大綱就可以按 Why - What - How 來展開:先告訴讀者為什么要設計該軟件,它解決了哪些問題。之后講述軟件的架構模型、關鍵模塊以及主線流程。最后詳細地講解具體實現。
在寫文章之初,我們的知識還不夠深入和整體,可以先寫 what 和 how 的部分,加深理解之后,就能明白設計的 why 了。
?描述設計原理,通過畫圖幫助分析設計意圖
在介紹原理和實現的時候,相比于貼代碼,更好的方式是通過畫圖來表達。代碼的確能體現全部的設計細節(jié),但代碼更重要的任務是作為知識和硬件指令之間的橋梁。相反,如果我們用圖表的形式表達設計意圖,就會對人類更友好,更容易閱讀、理解和學習。畫圖本身也是一種加深理解,去粗取精的過程。
下圖是我讀了 leveldb 之后畫的 leveldb 存儲架構圖:
作為存儲引擎,LSM Tree 的實現是 leveldb 的核心,leveldb 本身源碼已經很清晰、簡潔,但如果通過上面這樣一張圖來講述其 LSM Tree 的具體設計,一定會比貼代碼要易懂得多。
想一想,為什么要這么設計,好處在哪里?
當我們能用圖表和文字來表達出軟件的完整設計后,我們對代碼的理解已經比較透徹,甚至,讓我們自己來照著寫一個新的也不是不可能了。
這個時候,就應該進一步的思考,如果是我自己來解決問題,我會怎么做?我能比原作者做得更好嗎(通常不能)?
在思考為什么這么設計的時候,如果相關領域知識不充足,就會驅使我們去查找很多參考資料,了解和借鑒別人看問題的角度。找資料的過程總有驚喜,如果能讀到一些非常深入淺出的文章,而后就會懷著敬佩之情,收藏、關注作者的博客,想想如果不是因為讀了某段代碼,還真無緣遇到這些精彩的文章和優(yōu)秀的作者。
我在讀 Go 語言內存管理代碼的時候,一開始搞懂了 tcmalloc 的原理和實現,但對其所謂線程緩存、無鎖分配等等賣點理解不深刻。直到回過頭去讀了 CSAPP 動態(tài)內存分配的章節(jié),又結合 ptmalloc、jemalloc 的設計,相互對比理解,這才更清晰的認識了 tcmalloc 的設計決策。
經過這一階段的思考并結合其他人的理解之后,我們就能清楚地意識到,軟件所面臨問題的限制條件是什么,作者這樣設計的好處有哪些。把這部分寫完,添加到文章的最開始,就比較完美了。
5. 講個 Session,收獲 Extra Bonus
如果還有精力和興致,那不如把文章的內容提取出來做個 Session 講給大家,額外的付出能收獲額外的獎賞。
有過做講師經歷的同學肯定會知道,給別人講東西,收獲最大的不是聽眾,而是講師本人。想要輸出一小時的 Session,所花費的準備時間可能要十個小時。我們需要花費數倍于講解的時間來完善素材,理清思路,準備問題,甚至還包括思考可能會涉及到的拓展內容。做這些工作在提升我們 session 質量的同時,無形中也不斷地強化了我們對相關知識的認知。
梳理要點,邏輯自洽
一個 Session 成功的基礎在于能不能邏輯自洽。而邏輯自洽的前提就是關鍵要點必須清晰,并且前后可以呼應。
上一節(jié)提到的文章,正好就是 Session 材料的源泉,因此我會反復遍歷整篇文章,期望從中抽取所需的內容。這個過程往往伴隨著不斷地發(fā)現文章的內容缺失、邏輯不通之處,這時文章就得到了進一步的改善。所以經常發(fā)現,當整個 Slide 完整的順下來后,不僅成就感爆棚,文章也豐滿了,理解還更深刻了。
去粗取精,鍛煉表達
相比寫文章,講 Session 要我們進一步的去除細節(jié),只保留最核心的思想,這本身是對抽象能力的一種鍛煉。
另外,自己了解清楚,和能給別人講清楚是完全不同的兩種概念。如何能把核心知識講給聽眾,并且能讓聽眾更容易的聽懂,需要仔細地思考語言的表達。每一次成功的 Session 都是對自己表達能力的一次提升。
表達上最常見的問題就是照著文字念。我個人喜歡通過減少 Slide 中文字的數量,來倒逼自己提升表達的邏輯性與連貫性。可以嘗試思考,如果內容只是一張圖,那么要怎么講清楚這張圖,用這種辦法訓練表達能力。
揣摩聽眾感興趣的方向
考慮聽眾的感受也很重要,如果講的內容大家不感興趣,不愛聽,或是晦澀難懂,跟不上節(jié)奏,就容易導致整個 Session 反響寥寥,大家會覺得來聽你的 Session 浪費了自己的時間。
所以不僅要能講清楚,還要揣摩聽眾感興趣的方向。合理的設置內容,去除枯燥乏味而非關鍵性的東西,并且調整講解的順序,把易懂的、精彩的部分穿插放置,這樣就可以不斷地激發(fā)聽眾的興趣。
最后,線下組織的效果要比線上視頻講解好得多。在線下聽眾的注意力更集中,互動效果好,演講者也更容易通過聽眾的表情、神態(tài)來判斷是否需要調整內容和速度。如果因為條件限制一定要做視頻 Session,那么可能需要經常停頓下來問些問題,或是主動的尋求反饋。
6. 結語
本文是我日常讀代碼的一點經驗,總結下來,就是要:
- 仔細地選擇學習的項目;
- 先通過文檔了解全景,再逐步深入代碼;
- 找對抽象和邊界,能幫助我們建立思考模型;
- 寫篇文章講述代碼的設計,是深入理解代碼的好辦法;
- 自己學會了還不夠,能清楚地講給別人才是真正的掌握。
最后祝愿所有讀者都能從代碼中獲得最大的樂趣。