為什麼你的程式碼如此難以理解

臘八粥發表於2014-11-21

“我到底在想什麼?!?”

凌晨1:30分,我正盯著不到一個月前我寫的一段程式碼。當時它看起來像是件藝術品,全部是可理解的,優雅、簡單、讓人歎為觀止。這一切都不再了,明天是我的最後期限,數小時前我發現了一個bug。當時看起來的簡單和邏輯再也說不通了。可以肯定的是,如果我寫程式碼,我應該足以聰明到理解程式碼?

經過了多次這種經歷以後,我開始認真思考,為什麼我的程式碼在我編寫的時候很清楚、而當我數週或數月後回頭看的時候,它們卻那麼費解。

問題1,過度複雜的心智模型

為了理解當你間隔一段時間返回到你的程式碼、卻發現程式碼難以理解的第一步,就是理解我們如何從心智上建立問題模型。你寫的幾乎所有程式碼都是儘量解決現實世界的問題。在你寫程式碼之前,你需要理解你正試圖解決的問題。這常常是程式設計裡最難的一步。

為了解決現實世界的問題,我們首先需要形成該問題的心智模型【注1】,以此作為程式設計意圖。接下來你需要形成實現程式設計意圖的方案模型,我們姑且稱為語義模型(Semantic model)。從來不要混淆你的程式設計意圖和此意圖的方案。我們傾向於主要考慮方案方面的東東,而常常忽略意圖的模型。

你接下來的步驟是形成可能最簡單的語義模型。這是容易搞錯的第二步。如果你不花時間去真正理解你正試圖解決的問題,你將在寫程式碼時被絆倒在模型上。另一方面,如果你真正考慮了你正儘量做的事情,你經常得到一個非常簡單的模型,這足以讓你掌握最初的意圖。

如果你想容易地維護簡單的程式碼,就儘可能多些地消除意外的複雜性。我們正試圖解決的問題是足夠複雜的。如果你不必那麼做,就不要把意外的複雜性增加進來。

問題2,語義模型到程式碼的糟糕轉化

一旦你盡全力形成了最好的語義模型,那麼就到了把它轉化為程式碼的時候了。我們稱之為句法模型(syntactic model)。你正試圖把你的語義模型的意義轉化為計算機能夠理解的句法。

如果你有非常不錯的語義模型、而在轉化為程式碼時搞砸了,那麼在你需要在以後某個階段回頭修改程式碼時,你將比較痛苦。當你腦子裡還有語義模型時,把你程式碼對映到語義模型是容易的。回憶起變數“x”實際上代表一條記錄被建立的日期、而“y”程式碼記錄被刪除的日期,這是不難的。當你3個月後再回來看程式碼,你的腦子裡將沒有這個語義模型了,因此無法理解同樣的變數名字。

把語義模型轉化為句法的任務就是儘量多地留下線索,讓你在今後返回時,能夠重建當初的語義模型。

好了,你該怎麼做呢?

類結構和命名

如果你在使用物件導向語義,請儘量讓你的類結構和命名靠近語義模型。領域驅動設計(Domain Driven Design)【注2】是在這種練習上投入了相當重要性的一種運動。即使你沒有相信完全的DDD方法,你也應當非常小心地考慮類結構和命名。每個類都是你留給自己和其他人的線索,它幫助你在將來返回的時候重建你的心智模型。

變數、引數和方法命名

儘量避免普通的變數和方法命名。不要把方法命名為“Process”,因為“PaySalesCommision”更有意義。不要把變數命名為“x”,因為它應當是“currentContract”。不要把引數命名為“input”,因為“outstandingInvoices“更好。

單一功能原則(Single responsibility principle,簡稱SRP)

SRP【注3】是面對物件設計原則的核心之一,關聯著好的類和變數命名。它認為,任何類或方法都應該完成一個單一的功能,只能是一個單一的功能。如果你想為類和方法給出有意義的名字,那麼它們需要有一個唯一的較好定義的目的。如果一個單一類從資料庫讀和寫、計算營業稅、通知交易客戶並生成賬單,那麼你就可能無法給出合適的名字。我常常停留在重構類上,因為我總是努力取一個足夠短的名字,以描述它做的每個功能。為了更多地討論SRP和其它物件導向原則,可以參考我的博文《物件導向設計》。

適當的註釋

如果因為某種原因,你不能讓程式碼變得清晰,你同情將來的自己,需要不得不做些事情,那就留下注釋來說明你為什麼不得不那樣做。註釋傾向於快速地變得陳舊,因此我寧願儘可能讓程式碼自描述,註釋用來說明為什麼你不得不那樣做,而不是它如何做。

問題3,沒有足夠的組塊

心理學上的組塊被定義是,把資訊組塊定位為單一的實體。那麼這該如何應用到程式設計上呢?作為一名開發者,在你積累經驗時,你開始發現你解決方案裡反覆出現的模式。極具影響的設計模式:《可重用的物件導向軟體》是第一本整理和解釋一些模式的書。儘管如此,組塊不僅僅用在設計模式和麵向物件。在函數語言程式設計(FP)裡,存在大量的著名標準函式具備這同樣的目的。演算法是組塊的另一種形式(後續會更多)。

當你合理地使用組塊(設計模式、演算法和標準函式)時,它讓你停下來思考,你編寫的程式碼是如何執行的、而不是考慮它做了什麼。這縮短了你的語義模型(你的程式碼)和句法模型(你腦子裡的模型)的距離。這個距離越短,你就越容易重建你的心智模型。

如果你有興趣瞭解更多FP裡的函式,請移步到我的文章面向web開發者的函數語言程式設計

問題4,費解的用法

目前,我們主要討論瞭如何結構化你的類、方法和變數命名。心智模型的另一個重要部分是理解這些方法應該怎樣被使用。再次強調,當你最初形成心智模型時,這是相當清晰的。當你後來返回時,就非常難以重建你的類和方法的、所有有意圖的用法了。通常這是因為不同的用法散佈在你的程式其它地方。有時候甚至出現在很多不同的專案中。

我就是在這種情況下發現測試用例是非常有用的。除了相應地知道一個修改是否破壞了程式碼的明顯好處,測試為你的程式碼提供了一整套的用例。你不必搜遍100個檔案,只需看測試就能得到引用的全景。

注意為了達到這個目的,你需要有一整套完整的測試用例。如果你的測試僅僅覆蓋了一部分、而你認為測試是完整的,那麼你後來將陷入困境。

問題5,不同的模型之間沒有清晰的途徑

你的程式碼從技術角度看,常常是優秀的、非常優雅,但是從程式意圖到語義模型、再到程式碼存在非常不自然的跳躍。考慮你選擇的一堆模型的透明性是重要的。從程式意圖到語義模型、再到程式碼的過程需要儘可能平滑。你應當能夠看透對應到問題的每個模型的所有方面。多數情況下,最好選擇特定類結構或演算法不是為了它在隔離方面的優雅,而是可以連線各種模型,為你重建的目的而留下 一條自然的途徑。當你從抽象的程式設計意圖走到具體的程式碼時,你做的選擇應該受到 你能夠表現更為抽象模型 的清晰度驅使。

問題6,發明演算法

作為程式設計師,我們經常認為,我們在為了解決問題而發明著演算法。事實很難是這樣的。幾乎所有情況下,已經有現成的演算法可以被組合在一起解決你的問題了。像最短路徑搜尋法、字串相似度演算法、粒子群演算法等。大部分程式設計是以正確的組合、選擇現存演算法來解決你的問題。如果你正在發明新演算法,那麼,要麼你不知道合適的演算法、要麼你正忙於你的博士論文。

總結

最後總結:作為一名程式設計師,你的目標是建立能夠解決你問題的、儘可能簡單的語義模型。把語義模型儘可能靠近地轉化為句法模型(程式碼),儘可能多地提供線索,便於你之後無論哪個人看你的程式碼,都能重建像你最初腦子裡的、相同的語義模型。

設想一下,當你走過你程式碼的被照亮的森林時,你在身後留了麵包屑。相信我,當你需要找到回去的路時,森林將充滿了黑暗、朦朧和不詳的預感。

聽起來容易,實際做起來是很難的。

  • 原文地址:https://medium.com/on-coding/why-your-code-is-so-hard-to-understand-83057c115a2b
  • 注1:心智模型是用於解釋個體為現實世界中之某事所運作的內在認知歷程。http://zh.wikipedia.org/wiki/心智模型
  • 注2:要通過建立領域模型來加速複雜的軟體開發,就需要利用大量最佳實踐和標準模式在開發團隊中形成統一的交流語言;不僅重構程式碼,而且要重構程式碼底層的模型;同時採取反覆迭代的敏捷開發方法,深入理解領域特點,促進領域專家與程式設計師的良好溝通。http://baike.baidu.com/view/3705331.htm
  • 注3:馬丁把功能(職責)定義為:“改變的原因”,並且總結出一個類或者模組應該有且只有一個改變的原因。一個具體的例子就是,想象有一個用於編輯和列印報表的模組。這樣的一個模組存在兩個改變的原因。第一,報表的內容可以改變(編輯)。第二,報表的格式可以改變(列印)。這兩方面會的改變因為完全不同的起因而發生:一個是本質的修改,一個是表面的修改。單一功能原則認為這兩方面的問題事實上是兩個分離的功能,因此他們應該分離在不同的類或者模組裡。把有不同的改變原因的事物耦合在一起的設計是糟糕的。http://zh.wikipedia.org/wiki/單一功能原則

相關文章