Categories
程式開發

源碼分析怎麼做?


近日在知乎上看到一個關於源碼分析的提問《程序員閱讀源碼是一種什麼心態?源碼對編程意義何在?如何才能更好閱讀代碼?“》,正好結合對比讀完的兩本講解Spring 源碼的書《Spring 源碼深度解析(第2 版)》“(簡稱《Spring》)和《深入理解Spring MVC 源代碼》“(簡稱《Spring MVC》),聊一聊閱讀的體驗,順便講一講源碼分析怎麼做、怎麼寫這回事。

首先要明確的是,源碼分析的入手點是軟件而不是源代碼。軟件( Software )是一個寬泛的概念,包括應用程序、工具箱和框架等等。軟件可以說是由代碼組成的,那麼我們強調入手點是軟件而不是源代碼的原因是什麼呢?

源碼分析從源代碼入手,就容易落入具體實現的窠臼當中;而代碼構成的軟件整體,有其被創造的背景、要解決的問題、演進過程中面臨的困難和決策,以及最終所為用戶認知的形態。源碼分析從軟件整體入手,才能夠脫離技術人員對技術本身的痴迷的影響,從務實的角度講解代碼要解決什麼問題、代碼如何解決問題的以及代碼為什麼要這麼解決問題。

從這個角度講,《Spring》開篇講解Spring 框架的整體架構一節寫得較為出彩。它首先展示了一張Spring 框架中模塊劃分圖,大致如下。

源碼分析怎麼做? 1

我們且不論這張圖的時效性和準確性,但是通過這張圖我們很明確地能知道作者打算把Spring 大致分成核心容器模塊、面向切面編程模塊、數據訪問模塊和Web 應用模塊來講解。實際上,《Spring》第一篇對這些模塊簡明扼要的總結是全書僅有的的少數精華之一。

只不過在我看到《Spring MVC》中更新版的基本模塊劃分圖之後,我就知道《Spring》全書僅有的精華原來是來自全網總結的公認精華。這就跟某Flink 實戰書籍唯一算得上精華的第一章是因為它大體是對《Streaming 101》和《Streaming 102》的洗稿一樣。

回到主題,作為源碼分析書籍,從代碼的框架以及評論不同模塊的意圖入手是合適的。這跟我們此前強調的從軟件角度切入是不矛盾的,因為我們好歹沒看到代碼,而想要閱讀源碼分析的讀者大抵也對應用方式比較了解或不想了解,不需要刻意以實例引入。

在提綱挈領分析對象之後,其實就可以從實例和問題點出發逐級下降分析軟件了。不過囿於寫書的框架,大部分書籍會介紹環境的搭建這樣枯燥混字數的內容。在這一點上兩本書都有莫名其妙的構建細節,但總體控制在20 頁以內,也無可厚非。

前面提過,源碼分析的目的是講解代碼要解決什麼問題、代碼如何解決問題的以及代碼為什麼要這麼解決問題。在這個目標的實現上《Spring》和《Spring MVC》大相徑庭。

《Spring MVC》的講解按照目錄分成三個部分。

第一部分主要從使用的角度入手,由開發者最熟悉的功能切入,講解了基本組件包括控制器、模型和視圖在抽象層面上是如何被支持的。在此基礎上對MVC 模型最承擔邏輯的控制器展開了詳細的介紹,尤其是平時容易被終端開發者忽略的請求是如何進入框架和流程以及返回值是如何交付給請求方的。最後用簡短的篇幅簡略地介紹了WebFlux 的發展趨勢和一些常見的配置項。

第二部分接著從源碼切入,首先介紹了源碼閱讀的一些技術技巧,再對Spring MVC 框架的啟動、MVC 框架請求分發的核心DispatcherServlet 類的功能、RequestMapping 的查找原理和請求處理方法的執行過程一一進行具體的介紹。每個細節部分也是按照這種主題加解決方法的模式,先拋出一個問題,抽像地討論解決思路,再結合代碼講解關鍵細節,最後簡略地做完整性補充和擴展討論。

第三部分可以單獨拆開來,是在完成了源碼分析以後以一個常見的基於Spring MVC 實現微信公眾號快速開發框架的例子來介紹Spring MVC 的實用過程。通過分析時下熱點的具體實例,可以讓讀者清晰的看到前面所講的知識點在實踐中到底是怎麼被運用的,哪些一定會涉及且常常會被實現考慮在內,哪些是在哪種特定情況下會涉及的,以及哪些是平常看不到的或者不需要重點鑽研的。

結合每個部分論述的時候穿插的圖片、習慣性地分點闡述問題,以及代碼塊在樣式上的區分以及量上的克制(雖然有些地方還是多了),這本《深入理解Spring MVC 源代碼》確實值得推薦。

《Spring》則完全不同,在幾個方面上都踩了大坑。

第一個是囉嗦,代碼佔據了主要的篇幅,這是搞源碼分析最忌諱的事情。

源碼分析重要的是分析,是以軟件為切入點,從務實的角度,講解代碼要解決什麼問題、代碼如何解決問題的以及代碼為什麼要這麼解決問題,絕對不是大篇幅的複制粘貼真正的源代碼。

稍有經驗的開發者都知道,真正的源代碼大概只有1/5 是業務邏輯,其中核心邏輯少得可憐。剩下的4/5 中大概有2/5 是日誌等旁路邏輯,以及2/5 是數據校驗和異常處理。通俗地說,你啪地貼出來整頁整頁的源代碼,誰想看?誰一下看得懂啊?尤其是《Spring》裡面大段大段的貼XML 配置,水字數混厚度印廁紙也不能這麼搞啊。

你會在《Spring》中看到這種句子。

很遺憾,在這個代碼中我們還是沒有看到想要看到的代碼。是的,就是這句代碼,我們的歷程猶如剝洋蔥一樣,一層一層的直到最內部的代碼實現,雖然很簡單。這一功能委託給了某方法去實現(轉入某方法,又委託給了某某方法,轉入某某方法)。 …

我想也不用再往下列舉也能很簡單地看出問題來了。

我來讀你的源碼分析大作,當然是要聽你是怎麼看待這個軟件解決問題的方式的了。不是想跟著你就像我自己讀代碼一樣一行一行跳轉來跳轉去,搞清楚每行代碼的調用棧,搞清楚每個if-else 是要篩選哪個細枝末節的特化配置特殊情況。

《Spring》的行文讀下來就是作者自己讀代碼的時候的流水賬,沒什麼思考,全是機械的代碼跳轉動作和直譯if-else 內容。這麼些內容做成視頻教程都嫌囉嗦,不要說以書籍的方式展示了。

客觀來說,《Spring》裡還是有些有用的分析的,但是它們都淹沒在整頁整頁的代碼裡,淹沒在代碼行內註釋的評註裡了。

這裡就引出來第二個問題,不分輕重。

《Spring MVC》介紹WebFlux 框架的時候僅使用了15 頁的篇幅,從需求背景到技術背景,再到使用的不同和對比WebMVC 的工業應用都講出了最關鍵的要點。此外除了為了完整性講述的視圖知識,幾乎全都圍繞MVC 處理請求這個核心鏈路,從核心過程、核心類DispatcherServlet 以及核心註解RequestMapping 出發講解內容。所有的擴展性知識和旁路內容都相應地克制了篇幅。

《Spring》則不同。我想每個對Spring 框架稍有了解的開發者,都知道Spring 核心容器部分最重要的就是Bean 的創建和管理,也就是依賴注入和Bean 的生命週期流轉。 《Spring》在介紹Bean 的創建和管理之前,用神奇的剝洋蔥方式,帶領你剝了怎麼讀XML 文件、怎麼驗證XML 文件,怎麼解析XML 標籤等等亂七八糟的問題。

暫且不說XML 配置只是一種實現,現在註解驅動編程在Spring 社區已經佔據絕對上風。怎麼讀取、驗證XML 文件跟Spring 有什麼緊要的關係?這都是一些外圍的、旁路的知識,XML 的規範、細節和技巧自己都能寫出一本書來。

我們在編程的時候強調不要測試三方庫的行為,應該測試業務邏輯的行為。這是因為軟件之間應該有清晰的限界上下文。源碼分析也一樣,一本講Spring 的書大概率不會突然出現聊GC 的問題的時候聊到JVM 的垃圾回收器的細節裡面去,同樣的對於XML 這種依賴技術一筆帶過或者用少於一頁的篇幅介紹上面幾個問題的要點知識即可。不然,讀者究竟是來看你分析什麼源碼的呢?

在開發者不關心的內容上著墨過多,就會導致關鍵內容相比之下佔比減少。

《Spring》的作者在文中時不時咋呼某一行【藏得很深】的代碼有多麼重要的作用。殊不知這樣的代碼在代碼水平上是欠缺的。因為我們人主觀地第一眼難免會根據代碼的篇幅來判斷代碼內容的重要性,所以同等重要的代碼最好佔據同等的篇幅。在一堆不重要的邏輯中淹沒一行關鍵的代碼是不應當的。

當初我讀Flink 代碼裡實現stopWithSavepoint 功能如何通過毒丸消息逐步下發停止任務的時候,就被委託跳來跳去和藏在大型方法裡的那一句終止語句給坑到了,實在是不好發現。理想情況下,委託和代理應該只在需要的時候引入,同等級別的語句應該佔據相同的篇幅。上面的例子裡跟終止語句平級的那一坨內容應當被抽取到一個單獨的方法裡面去。

回到書評,Bean 的創建和加載,BeanFactory 的行為和ApplicationContext 的行為及其差異,這些重要的內容在《Spring》中跟XML 的讀取篇幅一致,比大部分開發者不會用的SpEL 甚至還少一些。這樣分析源代碼,難怪乎某些知乎用戶呼籲初學者別讀源代碼。早早地陷入到不重要的細節,抓小放大,蹉跎時間。這怎麼能行呢?

還有另一本Spring 講解的書叫《Spring 5 核心原理與30 個類手寫實戰》,這本書唯一有用的就是Spring 中常用的設計模式一節,還算有實踐的分析價值。其他的無論是30 頁的配環境還是手寫實戰裡又回到了各種代碼細節,不堪卒讀。

最後一點,是關於圖表的。

我們知道,人讀文字的效率是不如圖表的,圖表具有直觀的表達能力。 《Spring MVC》和《Spring》都有不少的圖表,但是在圖表的類型上卻有所不同。

《Spring MVC》的圖表多為模塊邏輯劃分圖,IDEA 截取的關鍵數據結構圖和瀏覽器截取的代碼效果圖;《Spring》的圖表多為UML 圖,在最後一章中大量使用了時序圖。

這其中最有意義的是模塊的邏輯劃分和交互圖,因為這個是我們從一個高的抽象層次來理解一個模塊的助力。在日常方案設計中,我也總會對需求設計的領域和內容做一個模塊的邏輯劃分圖以及交互圖來幫助自己梳理思路,並且用於方案評審和產品溝通,對於模塊劃分的意見分歧和產品意圖的理解偏差能夠很快被發現、討論並解決。

其次比較有意義的是時序圖。對於關鍵的交互流程來說,時序圖是一種細化的交互流程表達方式,往往能夠指導代碼的實現以及排查交互的問題。不過,在實踐當中我發現直接畫時序圖很容易過早的陷入細節,抽象改動需要不停改動​​倍數的細節。這一功能更好的高層次替代品是流程圖,即核心流程包括哪些關鍵步驟、關鍵判斷和關鍵結果。流程圖是邏輯步驟最小粒度的圖示,更有可能跟意圖改變做到一一對應。

UML 圖真的是沒什麼大用。 《領域驅動設計》裡面就提過,很多UML 圖在某些地方過於細緻,同時在某些地方又有很多遺漏。細緻的點在於人們認為必須將所有要編碼的對像都放到建模工具中,而細節過多的結果是不分輕重。 UML 圖已經是時代的眼淚了,請放過它。

源碼分析該怎麼做?從上面的對比閱讀裡總結下來的就一個中心和兩個基本點。

一個中心是源碼分析要從軟件入手而不是源代碼本身入手,從務實的角度分析代碼要解決什麼問題、代碼如何解決問題的以及代碼為什麼要這麼解決問題。

兩個基本點包括分析的過程要逐層深入,從高層次的抽象完整地講解,低層次的細節只需要講解要點;以及抓大放小,對旁路的、不重要的內容一筆帶過或完全不提。

最後在技巧上,不要選擇UML 圖或者復雜的時序圖作為輔助說明的手段,它們只會把事情變得更複雜;應該選擇模塊劃分圖、模塊交互圖和功能流程圖作為輔助說明的手段。不得不提的是,不要大段大段地貼源代碼!