代碼閱讀的姿勢

面向對象編程

眾里尋他千百度,驀然回首,那人卻在,燈火闌珊處。

一般地,在一個程序員的日常工作之中,絕大多數時間都是在「閱讀代碼」,而不是在「寫代碼」。但是,閱讀代碼往往是一件很枯燥的事情,尤其當遇到了一個不漂亮的設計,反抗的心理往往更加強烈。

事實上,變換一下習慣、思路和方法,代碼閱讀其實是一個很享受的過程。閱讀代碼的模式,實踐和習慣,集大成者莫過于希臘作者Diomidis Spinellis的經典之作:Code Reading, The Open Source Perspective.。本文從另外一個視角出發,談談我自己閱讀代碼的一些習慣,期待找到更多知音的共鳴。

工欲善其事,必先利其器

首先,閱讀代碼之前先準備好一個稱心如意的工具箱,包括IDE, UMLMind 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時,嘗試畫畫類圖,并體會它們之間關系,感受一下設計的美感,也是受益頗多的。

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算法為例。假設GFinalRDD,從后往前按照RDD的依賴關系,依次識別出各個Stage的起始邊界。

Stage劃分算法
  • Stage 3的劃分:

    1. GB之間是Narrow Dependency,規約為同一Stage(3);
    2. BA之間是Wide DependencyA為新的FinalRDD,遞歸調用此過程;
    3. GF之間是Wide DependencyF為新的FinalRDD,遞歸調用此過程;
  • Stage 1的劃分

    1. A沒有父親RDDStage(1)劃分結束。特殊地Stage(1)僅包含RDD A
  • Stage 2的劃分:

    1. RDD之間的關系都為Narrow Dependency,規約為同一個Stage(2);
    2. 直至RDD C, E,因沒有父親RDDStage(2)劃分結束;

最終,形成了Stage的依賴關系,依次提交Stage(TaskSet)TaskScheduler進行調度執行。

獨樂樂不如眾樂樂

與他人分享你的經驗,也許可以找到更多的啟發;尤其對于熟知該領域的人溝通,如果是Owner就更好了,更能得到意外的驚喜和收獲。

也可以通過各種渠道,收集他人的經驗,并結合自己的思考,推敲出自己的理解,如此才能將知識放入自己的囊中。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容