眾里尋他千百度,驀然回首,那人卻在,燈火闌珊處。
一般地,在一個程序員的日常工作之中,絕大多數時間都是在「閱讀代碼」,而不是在「寫代碼」。但是,閱讀代碼往往是一件很枯燥的事情,尤其當遇到了一個不漂亮的設計,反抗的心理往往更加強烈。
事實上,變換一下習慣、思路和方法,代碼閱讀其實是一個很享受的過程。閱讀代碼的模式,實踐和習慣,集大成者莫過于希臘作者Diomidis Spinellis
的經典之作:Code Reading, The Open Source Perspective.
。本文從另外一個視角出發,談談我自己閱讀代碼的一些習慣,期待找到更多知音的共鳴。
工欲善其事,必先利其器
首先,閱讀代碼之前先準備好一個稱心如意的工具箱,包括IDE
, UML
,Mind Maping
等工具。我主要使用的編程語言包括C++, Scala, Java, Ruby
;對于Scala, Java, Ruby
編程,我更偏向使用JetBrain
公司的產品;而對于C++
編程,我依然還在使用Eclipse
,因為Clion
的特性還沒有讓我滿意。
其次,高效地使用快捷鍵,這是一個良好的代碼閱讀習慣,它極大地提高了代碼閱讀的效率和質量。例如,查看類層次關系,函數調用鏈,方法引用點等等。
拔掉鼠標,減低對鼠標的依賴。當發現沒有鼠標而導致工作無法進行下去時,嘗試尋找對應的快捷鍵。通過日常的點滴積累,工作效率必然能夠得到成倍的提高。
力行而后知之真
閱讀代碼一種常見的反模式就是「通過Debug
的方式來閱讀代碼」。作者不推薦這種代碼閱讀的方式,其一,因為運行時線程間的切換很容易導致方向的迷失;其二,了解代碼調用棧對于理解系統行為并非見得有效,因為其包含太多實現細節,不易發現問題的本質。
但在閱讀代碼之前,有幾件事情是必須做的。其一,手動地構建一次工程,并運行測試用例;其二,親自動手寫幾個Demo
感受一下。
先將工程跑起來,目的不是為了Debug
代碼,而是在于了解工程構建的方式,及其認識系統的基本結構,并體會系統的使用方式。
如果條件允許,可以嘗試使用ATDD
的方式,發現和挖掘系統的行為。通過這個過程,將自己當成一個客戶,思考系統的行為,這是理解系統最重要的基石。
發現領域模型
發現「領域模型」是閱讀代碼最重要的一個目標,因為領域模型是系統的靈魂所在。通過代碼閱讀,找到系統本質的模型,并通過自己的模式表達出來,你才能真正地Hold
住了系統,否則一切都是空談。
首要的任務,就是找到系統的邊界,并能夠以「抽象的思維」思考外部系統的行為特征。其次,尋找系統潛在的,并能表達系統的重要概念,及其它們之間的關聯關系。
細節是魔鬼
糾結于細節,將導致代碼閱讀代碼的效率和質量大大折扣。例如,日志打印,解決Bug
的補丁實現,某版本分支的兼容方案,某變態用戶需求的錘子代碼等等。
閱讀代碼的一個常見的反模式就是「給代碼做批注」。這是一個高耗低效,投入產出比極低的實踐。越是優雅的系統,注釋越少;越是復雜的系統,再多的注釋也是于事無補。
我有一個代碼閱讀的習慣,為代碼閱讀建立一個單獨的code-reading
分支,一邊閱讀代碼,一邊刪除這些無關的代碼。
$ git checkout -b code-reading
刪除這些噪聲后,你會發現系統根本沒有想象之中那么復雜。事實上,系統的復雜性,往往都是之前不成熟的設計和實現導致的額外復雜度。
適可而止
閱讀代碼的一個常見的反模式就是「一根筋走到底,不到黃河絕不死心」。程序員都擁有一顆好奇心,總是對不清楚的事情感興趣。例如,消息是怎么發送出去的?任務調度工作原理是什么?數據存儲怎么做到的等等;雖然這種勇氣值得贊揚,但在代碼閱讀時絕對不值得鼓勵。
還有另外一個常見的反模式就是「追蹤函數調用棧」。這是一個極度枯燥的過程,常常導致思維的僵化;因為你永遠活在作者的陰影下,完全沒有自我。
我個人閱讀代碼的時候,函數調用棧深度絕不超過3
,然后使用抽象的思維方式思考底層的調用。因為我發現,隨著年齡的增長,曾今值得驕傲的記憶力,現在逐漸地變成自己的短板。當我嘗試追蹤過深的調用棧之后,之前的閱讀信息完全地消失記憶了。
也就是說,我更習慣于「廣度遍歷」,而不習慣于「深度遍歷」的閱讀方式。這樣,我才能找到系統隱晦存在的「分層概念」,并理順系統的結構。
發現她的美
三人行,必有我師焉。在代碼閱讀代碼時,當發現好的設計,包括實現模式,習慣用法等,千萬不要錯過;否則過上一段時間,這次代碼閱讀對你來說就沒有什么價值了。
當我發現一個好的設計時,我會嘗試使用類圖,狀態機,時序圖等方式來表達設計;如果發現潛在的不足,將自己的想法補充進去,將更加完美。
例如,當我閱讀Hamcrest
時,嘗試畫畫類圖,并體會它們之間關系,感受一下設計的美感,也是受益頗多的。
嘗試重構
因為這是一次代碼閱讀的過程,不會因為重構帶來潛在風險的問題。在一些復雜的邏輯,通過重構的等價變換可以將其變得更加明晰,直觀。
對于一個巨函數,我常常會提取出一個抽象的代碼層次,以便發現它潛在的本質邏輯。例如,這是一個ArrayBuffer
的實現,當需要在尾部添加一個元素時,既有的設計是這樣子的。
def +=(elem: A): this.type = {
if (size + 1 > array.length) {
var newSize: Long = array.length
while (n > newSize)
newSize *= 2
newSize = math.min(newSize, Int.MaxValue).toInt
val newArray = new Array[AnyRef](newSize)
System.arraycopy(array, 0, newArray, 0, size)
array = newArray
}
array(size) = elem.asInstanceOf[AnyRef]
size += 1
this
}
這段代碼給閱讀造成了極大的障礙,我會通過快速的函數提取,發現邏輯的主干。
def +=(elem: A): this.type = {
if (atCapacity)
grow()
addElement(elem)
}
至于atCapacity, grow, addElement
是怎么實現的,壓根不用關心,因為我已經達到閱讀代碼的效果了。
形式化
當閱讀代碼時,有部分人習慣畫程序的「流程圖」。相反,我幾乎從來不會畫「流程圖」,因為流程圖反映了太多的實現細節,而不能深刻地反映算法的本質。
我更傾向于使用「形式化」的方式來描述問題。它擁有數學的美感,簡潔的表達方式,及其高度抽象的思維,對挖掘問題本質極其關鍵。
例如,對于FizzBuzzWhizz
的問題,相對于冗長的文字描述,流程圖等方式,形式化的方式將更加簡單,并富有表達力。
以3, 5, 7
為輸入,形式化后描述后,可清晰地挖掘出問題的本質所在。
r1: times(3) => Fizz ||
times(5) => Buzz ||
times(7) => Whizz
r2: times(3) && times(5) && times(7) => FizzBuzzWhizz ||
times(3) && times(5) => FizzBuzz ||
times(3) && times(7) => FizzWhizz ||
times(5) && times(7) => BuzzWhizz
r3: contains(3) => Fizz
rd: others => string of others
spec: r3 || r2 || r1 || rd
實例化
實例化是認識問題的一種重要方法,當邏輯非常復雜時,一個簡單例子往往使自己豁然開朗。在理想的情況下,實例化可以做成自動化的測試用例,并以此描述系統的行為。
如果存在某個算法和實現都相當復雜時,也可以通過實例化探究算法的工作原理,這對于理解問題本身大有益處。
以Spark
中劃分DAG
算法為例。假設G
為FinalRDD
,從后往前按照RDD
的依賴關系,依次識別出各個Stage
的起始邊界。
-
Stage 3
的劃分:-
G
與B
之間是Narrow Dependency
,規約為同一Stage(3)
; -
B
與A
之間是Wide Dependency
,A
為新的FinalRDD
,遞歸調用此過程; -
G
與F
之間是Wide Dependency
,F
為新的FinalRDD
,遞歸調用此過程;
-
-
Stage 1
的劃分-
A
沒有父親RDD
,Stage(1)
劃分結束。特殊地Stage(1)
僅包含RDD A
;
-
-
Stage 2
的劃分:- 因
RDD
之間的關系都為Narrow Dependency
,規約為同一個Stage(2)
; - 直至
RDD C, E
,因沒有父親RDD
,Stage(2)
劃分結束;
- 因
最終,形成了Stage
的依賴關系,依次提交Stage(TaskSet)
至TaskScheduler
進行調度執行。
獨樂樂不如眾樂樂
與他人分享你的經驗,也許可以找到更多的啟發;尤其對于熟知該領域的人溝通,如果是Owner
就更好了,更能得到意外的驚喜和收獲。
也可以通過各種渠道,收集他人的經驗,并結合自己的思考,推敲出自己的理解,如此才能將知識放入自己的囊中。