簡介: 從團隊的角度來看,寫好程式碼是一件非常有必要的事情。如何寫出乾淨優雅的程式碼是個很困難的課題,我沒有找到萬能的 solution,更多的是一些 trade off,可以稍微討論一下。
寫了多年的程式碼,始終覺得如何寫出乾淨優雅的程式碼並不是一件容易的事情。按 10000 小時刻意訓練的定理,假設每天 8 小時,一個月 20 天,一年 12 個月,大概也需要 5 年左右的時間成為大師。其實我們每天的工作中真正用於寫程式碼的時間不可能有 8 個小時,並且很多時候是在完成任務,在業務壓力很大的時候,可能想要達到的目標是如何儘快的使得功能 work 起來,程式碼是否乾淨優雅非常可能沒有能放在第一優先順序上,而是怎麼快怎麼來。
在這樣的情況下是非常容易欠下技術債的,時間長了,這樣的程式碼基本上無法維護,只能推倒重來,這個成本是非常高的。欠債要還,只是遲早的問題,並且等到要還的時候還要賠上額外的不菲的利息。還債的有可能是自己,也有可能是後來的繼任者,但都是團隊在還債。所以從團隊的角度來看,寫好程式碼是一件非常有必要的事情。如何寫出乾淨優雅的程式碼是個很困難的課題,我沒有找到萬能的 solution,更多的是一些 trade off,可以稍微討論一下。
程式碼是寫給人看的還是寫給機器看的?
在大部分的情況下我會認為程式碼是寫給人看的。雖然程式碼最後的執行者是機器,但是實際上程式碼更多的時候是給人看的。我們來看看一段程式碼的生命週期:開發 --> 單元測試 --> Code Review --> 功能測試 --> 效能測試 --> 上線 --> 運維、Bug 修復 --> 測試上線 --> 退休下線。開發到上線的時間也許是幾周或者幾個月,但是線上運維、bug 修復的週期可以是幾年。
在這幾年的時間裡面,幾乎不可能還是原來的作者在維護了。繼任者如何能理解之前的程式碼邏輯是極其關鍵的,如果不能維護,只能自己重新做一套。所以在專案中我們經常能見到的情況就是,看到了前任的程式碼,都覺得這是什麼垃圾,寫的亂七八糟,還是我自己重寫一遍吧。就算是在開發的過程中,需要別人來 Code Review,如果他們都看不懂這個程式碼,怎麼來做 Review 呢。還有你也不希望在休假的時候,因為其他人看不懂你的程式碼,只好打電話求助你。這個我印象極其深刻,記得我在工作不久的時候,一次回到了老家休假中,突然同事打電話來了,出現了一個問題,問我該如何解決,當時電話還要收漫遊費的,非常貴,但是我還不得不支援直到耗光我的電話費。
所以程式碼主要還是寫給人看的,是我們的交流的途徑。那些非常好的開源的專案雖然有文件,但是更多的我們其實還是看他的原始碼,如果開源專案裡面的程式碼寫的很難讀,這個專案也基本上不會火。因為程式碼是我們開發人員交流的基本途徑,甚至可能口頭討論不清楚的事情,我們可以通過程式碼來說清楚。程式碼的可讀性我覺得是第一位的。各個公司估計都有自己的程式碼規範,遵循相關的規範保持程式碼風格的統一是第一步(推薦谷歌程式碼規範和微軟程式碼規範)。規範裡一般都包括瞭如何進行變數、類、函式的命名,函式要儘量短並且保持原子性,不要做多件事情,類的基本設計的原則等等。另外一個建議是可以多參考學習一下開源專案中的程式碼。
KISS (Keep it simple and stupid)
一般大腦工作記憶的容量就是 5-9 個,如果事情過多或者過於複雜,對於大部分人來說是無法直接理解和處理的。通常我們需要一些輔助手段來處理複雜的問題,比如做筆記、畫圖,有點類似於在記憶體不夠用的情況下我們借用了外存。
學 CS 的同學都知道,外存的訪問速度肯定不如記憶體訪問速度。另外一般來說在邏輯複雜的情況下出錯的可能要遠大於在簡單的情況下,在複雜的情況下,程式碼的分支可能有很多,我們是否能夠對每種情況都考慮到位,這些都有困難。為了使得程式碼更加可靠,並且容易理解,最好的辦法還是保持程式碼的簡單,在處理一個問題的時候儘量使用簡單的邏輯,不要有過多的變數。
但是現實的問題並不會總是那麼簡單,那麼如何來處理複雜的問題呢?與其借用外存,我更加傾向於對複雜的問題進行分層抽象。網路的通訊是一個非常複雜的事情,中間使用的裝置可以有無數種(手機,各種 IOT 裝置,桌上型電腦,laptop,路由器,交換機...), OSI 協議對各層做了抽象,每一層需要處理的情況就都大大地簡化了。通過對複雜問題的分解、抽象,那麼我們在每個層次上要解決處理的問題就簡化了。其實也類似於演算法中的 divide-and-conquer, 複雜的問題,要先拆解掉變成小的問題,從而來簡化解決的方法。
KISS 還有另外一層含義,“如無必要,勿增實體” (奧卡姆剃刀原理)。CS 中有一句 "All problems in computer science can be solved by another level of indirection", 為了系統的擴充套件性,支援將來的一些可能存在的變化,我們經常會引入一層間接層,或者增加中間的 interface。在做這些決定的時候,我們要多考慮一下是否真的有必要。增加額外的一層給我們的好處就是易於擴充套件,但是同時也增加了複雜度,使得系統變得更加不可理解。對於程式碼來說,很可能是我這裡呼叫了一個 API,不知道實際的觸發在哪裡,對於理解和除錯都可能增加困難。
KISS 本身就是一個 trade off,要把複雜的問題通過抽象和分拆來簡單化,但是是否需要為了保留變化做更多的 indirection 的抽象,這些都是需要仔細考慮的。
DRY (Don't repeat yourself)
為了快速地實現一個功能,知道之前有類似的,把程式碼 copy 過來修改一下就用,可能是最快的方法。但是 copy 程式碼經常是很多問題和 bug 的根源。有一類問題就是 copy 過來的程式碼包含了一些其他的邏輯,可能並不是這部分需要的,所以可能有冗餘甚至一些額外的風險。
另外一類問題就是在維護的時候,我們其實不知道修復了一個地方之後,還有多少其他的地方還需要修復。在我過去的專案中就出現過這樣的問題,有個問題明明之前做了修復,過幾天另外一個客戶又提了類似的問題出現的另外的路徑上。相同的邏輯要儘量只出現在一個地方,這樣有問題的時候也就可以一次性地修復。這也是一種抽象,對於相同的邏輯,抽象到一個類或者一個函式中去,這樣也有利於程式碼的可讀性。
是否要寫註釋
個人的觀點是大部分的程式碼儘量不要註釋。程式碼本身就是一種交流語言,並且一般來說程式語言比我們日常使用的口語更加的精確。在保持程式碼邏輯簡單的情況下,使用良好的命名規範,程式碼本身就很清晰並且可能讀起來就已經是一篇良好的文章。特別是 OO 的語言的話,本身 object(名詞)加 operation(一般用動詞)就已經可以說明是在做什麼了。重複一下把這個操作的名詞放入註釋並不會增加程式碼的可讀性。並且在後續的維護中,會出現修改了程式碼,卻並不修改註釋的情況出現。在我做的很多 Code Review 中我都看到過這樣的情況。儘量把程式碼寫的可以理解,而不是通過註釋來理解。
當然我並不是反對所有的註釋,在公開的 API 上是需要註釋的,應該列出 API 的前置和後置條件,解釋該如何使用這個 API,這樣也可以用於自動產品 API 的文件。在一些特殊優化邏輯和負責演算法的地方加上這些邏輯和演算法的解釋還是非常有必要的。
一次做對,不要相信以後會 Refactoring
通常來說在程式碼中寫上 TODO,等著以後再來 refactoring 或者改進,基本上就不會再有以後了。我們可以去我們的程式碼庫裡面搜尋一下 TODO,看看有多少,並且有多少是多少年前的,我相信這個結果會讓你很驚訝(歡迎大家留言分享你查詢之後的結果)。
儘量一次就做對,不要相信以後還會回來把程式碼 refactoring 好。人都是有惰性的,一旦完成了當前的事情,move on 之後再回來處理這些概率就非常小了,除非下次真的需要修改這些程式碼。如果說不會再回來,那麼這個 TODO 也沒有什麼意義。如果真的需要,就不要留下這個問題。我見過有的人留下了一個 TODO,throw 了一個 not implemented 的 exception,然後幾天之後其他同學把這個程式碼帶上線了,直接掛掉的情況。儘量不要 TODO, 一次做好。
是否要寫單元測試?
個人的觀點是必須,除非你只是做 prototype 或者快速迭代扔掉的程式碼。
Unit tests are typically automated tests written and run by software developers to ensure that a section of an application (known as the "unit") meets its design and behaves as intended. In procedural programming, a unit could be an entire module, but it is more commonly an individual function or procedure. In object-oriented programming, a unit is often an entire interface, such as a class, but could be an individual method.From Wikipedia
單元測試是為了保證我們寫出的程式碼確實是我們想要表達的邏輯。當我們的程式碼被整合到大專案中的時候,之後的整合測試、功能測試甚至 e2e 的測試,都不可能覆蓋到每一行的程式碼了。如果單元測試做的不夠,其實就是在程式碼裡面留下一些自己都不知道的黑洞,哪天呼叫方改了一些東西,走到了一個不常用的分支可能就掛掉了。我之前帶的專案中就出現過類似的情況,程式碼已經上線幾年了,有一次稍微改了一下呼叫方的引數,覺得是個小改動,但是上線就掛了,就是因為遇到了之前根本沒有人測試過的分支。單元測試就是要保證我們自己寫的程式碼是按照我們希望的邏輯實現的,需要儘量的做到比較高的覆蓋,確保我們自己的程式碼裡面沒有留下什麼黑洞。關於測試,我想單獨開一篇討論,所以就先簡單聊到這裡。
要寫好程式碼確實是已經非常不容易的事情,需要考慮正確性、可讀性、魯棒性、可測試性、可以擴充套件性、可以移植性、效能。前面討論的只是個人覺得比較重要的入門的一些點,想要寫好程式碼需要經過刻意地考慮和練習才能真正達到目標!