開發與維運

如何寫好代碼?

image.png
寫了多年的代碼,始終覺得如何寫出乾淨優雅的代碼並不是一件容易的事情。按10000小時刻意訓練的定理,假設每天8小時,一個月20天,一年12個月,大概也需要5年左右的時間成為大師。其實我們每天的工作中真正用於寫代碼的時間不可能有8個小時,並且很多時候是在完成任務,在業務壓力很大的時候,可能想要達到的目標是如何儘快的使得功能work起來,代碼是否乾淨優雅非常可能沒有能放在第一優先級上,而是怎麼快怎麼來。

在這樣的情況下是非常容易欠下技術債的,時間長了,這樣的代碼基本上無法維護,只能推倒重來,這個成本是非常高的。欠債要還,只是遲早的問題,並且等到要還的時候還要賠上額外的不菲的利息。還債的有可能是自己,也有可能是後來的繼任者,但都是團隊在還債。所以從團隊的角度來看,寫好代碼是一件非常有必要的事情。如何寫出乾淨優雅的代碼是個很困難的課題,我沒有找到萬能的solution,更多的是一些trade off,可以稍微討論一下。

代碼是寫給人看的還是寫給機器看的?

在大部分的情況下我會認為代碼是寫給人看的。雖然代碼最後的執行者是機器,但是實際上代碼更多的時候是給人看的。我們來看看一段代碼的生命週期:開發 --> 單元測試 --> Code Review --> 功能測試 --> 性能測試 --> 上線 --> 運維、Bug修復 --> 測試上線 --> 退休下線。開發到上線的時間也許是幾周或者幾個月,但是線上運維、bug修復的週期可以是幾年。

在這幾年的時間裡面,幾乎不可能還是原來的作者在維護了。繼任者如何能理解之前的代碼邏輯是極其關鍵的,如果不能維護,只能自己重新做一套。所以在項目中我們經常能見到的情況就是,看到了前任的代碼,都覺得這是什麼垃圾,寫的亂七八糟,還是我自己重寫一遍吧。就算是在開發的過程中,需要別人來Code Review,如果他們都看不懂這個代碼,怎麼來做Review呢。還有你也不希望在休假的時候,因為其他人看不懂你的代碼,只好打電話求助你。這個我印象極其深刻,記得我在工作不久的時候,一次回到了老家休假中,突然同事打電話來了,出現了一個問題,問我該如何解決,當時電話還要收漫遊費的,非常貴,但是我還不得不支持直到耗光我的電話費。

所以代碼主要還是寫給人看的,是我們的交流的途徑。那些非常好的開源的項目雖然有文檔,但是更多的我們其實還是看他的源碼,如果開源項目裡面的代碼寫的很難讀,這個項目也基本上不會火。因為代碼是我們開發人員交流的基本途徑,甚至可能口頭討論不清楚的事情,我們可以通過代碼來說清楚。代碼的可讀性我覺得是第一位的。各個公司估計都有自己的代碼規範,遵循相關的規範保持代碼風格的統一是第一步(推薦谷歌代碼規範[1]和微軟代碼規範[2])。規範裡一般都包括瞭如何進行變量、類、函數的命名,函數要儘量短並且保持原子性,不要做多件事情,類的基本設計的原則等等。另外一個建議是可以多參考學習一下開源項目中的代碼。

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的測試,都不可能覆蓋到每一行的代碼了。如果單元測試做的不夠,其實就是在代碼裡面留下一些自己都不知道的黑洞,哪天調用方改了一些東西,走到了一個不常用的分支可能就掛掉了。我之前帶的項目中就出現過類似的情況,代碼已經上線幾年了,有一次稍微改了一下調用方的參數,覺得是個小改動,但是上線就掛了,就是因為遇到了之前根本沒有人測試過的分支。單元測試就是要保證我們自己寫的代碼是按照我們希望的邏輯實現的,需要儘量的做到比較高的覆蓋,確保我們自己的代碼裡面沒有留下什麼黑洞。關於測試,我想單獨開一篇討論,所以就先簡單聊到這裡。

要寫好代碼確實是已經非常不容易的事情,需要考慮正確性、可讀性、魯棒性、可測試性、可以擴展性、可以移植性、性能。前面討論的只是個人覺得比較重要的入門的一些點,想要寫好代碼需要經過刻意地考慮和練習才能真正達到目標!

最後

歡迎各位技術同路人加入阿里云云監控(CloudMonitor)團隊,我們專注於解決雲上服務和資源的可觀測性問題,並和雲上的運維工具進行整合,致力於為企業、開發者提供一站式的智能監控運維服務,內推直達郵箱:[email protected]

相關鏈接

[1]https://google.github.io/styleguide/
[2]https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/

Leave a Reply

Your email address will not be published. Required fields are marked *