導讀:為趕項目進度欠下一堆技術債怎么辦?業務邏輯復雜,如何處理比較好?相似的功能要不要copy修改一下復用?怎么寫代碼注釋?好代碼無論對個人還是團隊都至關重要,然而要寫好代碼卻是一件非常不容易的事情,需要長期的經驗積累和學習。關于寫好代碼,本文作者分享了6個入門的比較重要的點,希望對同學們有所啟發。
寫了多年的代碼,始終覺得如何寫出干凈優雅的代碼并不是一件容易的事情。按10000小時刻意訓練的定理,假設每天8小時,一個月20天,一年12個月,大概也需要5年左右的時間成為大師。其實我們每天的工作中真正用于寫代碼的時間不可能有8個小時,并且很多時候是在完成任務,在業務壓力很大的時候,可能想要達到的目標是如何盡快的使得功能work起來,代碼是否干凈優雅非常可能沒有能放在第一優先級上,而是怎么快怎么來。
在這樣的情況下是非常容易欠下技術債的,時間長了,這樣的代碼基本上無法維護,只能推倒重來,這個成本是非常高的。欠債要還,只是遲早的問題,并且等到要還的時候還要賠上額外的不菲的利息。還債的有可能是自己,也有可能是后來的繼任者,但都是團隊在還債。所以從團隊的角度來看,寫好代碼是一件非常有必要的事情。如何寫出干凈優雅的代碼是個很困難的課題,我沒有找到萬能的solution,更多的是一些trade off,可以稍微討論一下。
代碼是寫給人看的還是寫給機器看的?
在大部分的情況下我會認為代碼是寫給人看的。雖然代碼最后的執行者是機器,但是實際上代碼更多的時候是給人看的。我們來看看一段代碼的生命周期:開發 --> 單元測試 --> Code Review --> 功能測試 --> 性能測試 --> 上線 --> 運維、Bug修復 --> 測試上線 --> 退休下線。開發到上線的時間也許是幾周或者幾個月,但是線上運維、bug修復的周期可以是幾年。
在這幾年的時間里面,幾乎不可能還是原來的作者在維護了。繼任者如何能理解之前的代碼邏輯是極其關鍵的,如果不能維護,只能自己重新做一套。所以在項目中我們經常能見到的情況就是,看到了前任的代碼,都覺得這是什么垃圾,寫的亂七八糟,還是我自己重寫一遍吧。就算是在開發的過程中,需要別人來Code Review,如果他們都看不懂這個代碼,怎么來做Review呢。還有你也不希望在休假的時候,因為其他人看不懂你的代碼,只好打電話求助你。這個我印象極其深刻,記得我在工作不久的時候,一次回到了老家休假中,突然同事打電話來了,出現了一個問題,問我該如何解決,當時電話還要收漫游費的,非常貴,但是我還不得不支持直到耗光我的電話費。
所以代碼主要還是寫給人看的,是我們的交流的途徑。那些非常好的開源的項目雖然有文檔,但是更多的我們其實還是看他的源碼,如果開源項目里面的代碼寫的很難讀,這個項目也基本上不會火。因為代碼是我們開發人員交流的基本途徑,甚至可能口頭討論不清楚的事情,我們可以通過代碼來說清楚。代碼的可讀性我覺得是第一位的。各個公司估計都有自己的代碼規范,遵循相關的規范保持代碼風格的統一是第一步,推薦Google Style Guides | styleguide和Framework Design Guidelines | Microsoft Docs。規范里一般都包括了如何進行變量、類、函數的命名,函數要盡量短并且保持原子性,不要做多件事情,類的基本設計的原則等等。另外一個建議是可以多參考學習一下開源項目中的代碼。
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的測試,都不可能覆蓋到每一行的代碼了。如果單元測試做的不夠,其實就是在代碼里面留下一些自己都不知道的黑洞,哪天調用方改了一些東西,走到了一個不常用的分支可能就掛掉了。我之前帶的項目中就出現過類似的情況,代碼已經上線幾年了,有一次稍微改了一下調用方的參數,覺得是個小改動,但是上線就掛了,就是因為遇到了之前根本沒有人測試過的分支。單元測試就是要保證我們自己寫的代碼是按照我們希望的邏輯實現的,需要盡量的做到比較高的覆蓋,確保我們自己的代碼里面沒有留下什么黑洞。關于測試,我想單獨開一篇討論,所以就先簡單聊到這里。
要寫好代碼確實是已經非常不容易的事情,需要考慮正確性、可讀性、魯棒性、可測試性、可以擴展性、可以移植性、性能。前面討論的只是個人覺得比較重要的入門的一些點,想要寫好代碼需要經過刻意地考慮和練習才能真正達到目標!