如何編寫高質量的程序
學習任何編程語言都會有一個基本的過程,開始的時候學習基本的語法,然後學習各種庫,框架,開始做各種項目。在做項目的過程中,隨着代碼量的增加,我們會漸漸感到失去對程序的掌控能力,bug開始增加,牽一髮而動全身,顧此失彼。這充分說明了編寫高質量程序的重要性,這裏的“高質量”主要指程序的正確性,可讀性,可維護性。
什麼是高質量的程序
正確性
程序正確性的重要程度無需多言,尤其在一些特殊領域,例如芯片製造業,航天業,武器製造業,對程序正確性往往有着極其嚴格的要求,因爲一旦程序出錯,代價往往是巨大的。在這些領域,需要使用形式化方法(formal methods)來自動驗證程序的正確性,也就是說你需要證明程序的正確性,而不僅僅保證程序在大多數情況下是正確的。在其它領域,對正確性沒有這麼高要求,形式化方法也不適用,但是我們還是需要使用其它手段,例如測試,code review等等來保證軟件的正確性。
可讀性
可讀性可以幫助程序作者理清思路,思路清晰後,程序不容易出錯。另外,其它程序員在維護你的代碼時,更容易理解你的意思,方便修改bug,方便擴展。
不要浪費自己的時間,更不要浪費別人的時間。
可維護性
這裏的可維護性主要指程序應對變化的能力。程序在完成基本功能後,可能會發生各種改變:用戶需求變了,性能達不到要求需要重新實現算法,等等。一旦程序的一個點發生改變,其它點如果也需要同時手動改變,那麼程序會變的不可控制,出bug的機會會增加。想像一下,我們的程序是一個盒子,在添加新功能時,如果只需要把新模塊插到一個地方,新模塊就可以被系統使用,這樣的程序可維護性是很高的。但是如果添加新功能時,需要把原來的程序盒子拆開,其它模塊也需要相應修改,才能加入新模塊,這樣的程序可維護性就很差。
提高程序質量的重要措施
測試
爲什麼強調自動化測試,而不是手動測試?因爲自動化測試可以增加測試的便捷度,而人們通常會更多地使用那些便捷度高的東西。我在做個人項目的時候就發現,在編寫了自動測試的腳本後,我每改動一點程序,就會自動運行一下腳本,在此之前,我明知道測試很重要,但是還是不會測試的如此頻繁。這樣的好處是可以方便定位bug,否則在系統經過了大量改動之後,出了bug都不知道可能在哪裏。
在對程序進行重構時,很重要的一點就在於,一定要先寫好測試用例,然後每改動一點,就自動測試一下,保證程序始終保持在可控狀態。
良好的編程風格
良好的編程風格,可以增強程序的可讀性,一個結構清晰的程序,你會更容易從中發現錯誤。另一方面,當程序發生變化時,很可能引入新的bug,良好的編程風格可以減少這種bug的出現。下面是與編程風格相關的一些措施。
- 風格指南
找一份你使用的編程語言的風格指南,例如Google的編程語言風格指南系列,Python的PEP8,並一直遵守這份指南的內容,如果有自動化工具幫助你保持這種風格,那再好不過。
- 最佳實踐
尋找你所使用語言的最佳實踐,他們可讀性強,經過了大量實踐的考驗,被廣泛接受,所以儘可能多地使用他們。
例如Python 的 The Hitchhiker's Guide to Python
- 起一個好名字
變量,函數名,類名,都需要一個好名字。程序本身是對解決方案的一種描述,一個好的名字會增強這種描述性,也會讓你的思維集中於解決方案,同時讓其它人更容易理解你的解決方案。
- 不要直接使用常量
在程序中直接使用的常量,一般被稱爲 Magic Numbers, 一方面它不利於其它程序員對程序的理解,因爲沒有人知道這個常量代表什麼。另一方面,多個常量之間可能是有關係的,直接使用常量根本反應不出這種關係。
- 同一變量名不要有多種含義
首先這種做法降低了可讀性,一個變量前面一個含義,後面一個含義,這會給閱讀程序的人帶來困擾。
- 儘可能保證變量作用域小
儘量減少變量定義的點與變量最後一次使用的點之間的跨度,這樣可以使變量與其相關代碼變得緊湊,提高可讀性,不用在使用變量時再去很多的地方查看其它引用。
- 保證函數短小精悍
過長的函數會讓讀者陷入細節的泥潭,還需要前後來回看才能明白前面一大段和後面一大段代碼的關係。將函數分解,然後給函數起一個好名字,讀者馬上就能明白這段代碼在做什麼。
提高應變能力
程序應對變化的能力強,可擴展性就強,也更容易在變化時保證正確性,這樣的程序可維護性強。下面是一些提高程序應變能力的措施。
- 不要使用常量
不要使用常量的另一個原因在於常量可能變化,如果程序中多次引入了這個常量,那麼一旦這個常量要發生變化,就需要同時改動許多地方,這時候,如果有些地方沒有改,就會使程序不一致,可能引入bug。
- 同一變量名不要有多種含義
同一變量名不要有多種含義另一個原因在於,多種含義之間可能會相互影響,第一次寫程序時你可能記得這些影響,但是以後對程序進行改動的時候,你可能就忘記了。例如函數內一段代碼執行後,索引i
的值等於一個長度,但是這段代碼後,你沒有將i
賦值給另一個變量len
,而是直接使用它。等過一段時間後,你或者其它人修改這段程序時,很可能忘了這段代碼執行後i
的值需要等於一個長度,因爲這是一種隱式的約定,所以很容易被忽視。
- 儘可能保證變量作用域小
保證變量作用域小也有利於重構。當一個函數變得很長時,你可能需要將它分解成多個函數,這時候,如果變量跨度小,就可以很方便地提取函數,不用來回查找與此函數相關的變量的引用。
- 減少代碼重複
如果有一段代碼在很多地方重複,這就告訴你,需要把他們提取成一個函數。因爲代碼的重複意味着這是一塊獨立的邏輯,獨立的邏輯可以抽象成一個函數。另一方面,一旦這段邏輯需要發生變化,只需要修改這個函數就可以了,不需要把所有地方都手動修改一遍。
- 數據驅動
數據驅動的意思是用數據表示來代替程序邏輯。例如,我們需要一個程序,判斷某個月有幾天,在實現時,最好用一個數組表示各個月的天數,需要哪個月直接查詢就好,而不要使用大量的if
語句來作邏輯判斷。這只是一個小例子,它提醒我們,如果程序中含有大量判斷語句,就應該想一想,能不能用數據來驅動邏輯,這樣需要修改的時候,我們直接修改數據就好,而不用修改程序邏輯。
我曾經接手過一個項目,這個項目其實是一個工具集,根據用戶的選擇,調用不同的工具。原始的代碼裏,就使用了大量if語句,並且每個工具其實調用方式和代碼都很相似。這樣,我每次添加新工具時,就需要找到多個if
語句塊,作相應修改。如果用數據驅動的話,我們完全可以去掉這些if
語句,在用戶的選擇與工具之間建立對應關係,這樣每當新添加工具時,只需要把工具加到系統裏,系統會根據這個表直接找到這個工具。這其實和之前舉的盒子的例子很相似,添加新工具時,只需要把工具插到盒子上的槽上,根本不用打開盒子。這就大大提高了程序的可擴展性。
控制複雜度
要保證軟件的高質量,很重要的一方面在於控制複雜度。控制複雜度的一個很重要的手段在於分解複雜的事物。我們之所以覺得一個事物複雜,是因爲同一時間需要關心的事情太多,把複雜事物分解後,每次我們只需要關心很少的事情,這樣就控制住了複雜度。
- 不要使函數或類過大
如果一個函數或類過大,他們會變得過分複雜,你同一時間需要關心許多細節。將函數或類變小之後,你的思維在一段時間內可以集中在同一個抽象層次,而不必過於深入其細節,這樣更容易發現程序中的缺陷,因爲你每次只需要關心很少的事情。在最高層,你只需要關心模塊之間的關係,關心算法的流程,不必關心模塊內部的事情。在最低層,你只需要關心一個模塊內部的事情,而不必關心其它事情。
- 不要使函數參數過多
函數參數過多可能說明這個函數負責了太多的事情,你需要將這個函數分解。另一方面,你需要從邏輯上考慮,這些參數是不是一個整體,如果是一個整體,那麼直接傳過來一個結構體,或者傳過來一個對象,是不是更合適?
- 不要使抽象層次過多
如果一個函數或類被分解爲過多的抽象層次,在模塊內部,你確實只需要關心很小的事情,但是這時候,由於模塊過多,抽象層次過深,他們之間的關係又使複雜度增長起來。
使用自動化工具
自動化工具迫使我們養成良好的編程習慣,而且不容易出錯。再次強調:
工具越是使用方便,你越會頻繁使用它。
所以,儘可能地讓你的工具使用便捷。 例如:
- 使用一些靜態檢測工具在編輯時自動幫助你檢測程序的不良風格
- 使用靜態檢測工具檢測程序常見錯誤
- 使用重構工具幫助你重構
- 使用自動化測試工具在保存時自動運行測試
注意事項
沒有什麼事情是一成不變的,所有的法則都需要考慮具體的情況。如果你要用一個法則,需要真正明白自己爲什麼要用,需要去權衡,而不要爲了能用上這個法則而生搬硬套。
好好問問自己:
- 變化真的存在麼?
- 真的需要抽象麼?
- 真的需要面向對象麼?
- 真的xxx麼?
參考資料
這篇文章是我這段時間閱讀過一些書後的想法,書目有
- 代碼大全(Code Complete)
- 重構——改善既有代碼的設計(Refactoring Improving the Design of Existing Code)
- 程序設計實踐(The Proactice of Programming)
在閱讀這些書的同時,我還在維護其它人的代碼,做自己的個人項目。在閱讀的過程中,我會不斷地想到我做的項目哪裏有問題,可以用書中提到的方法去修改,因此印象深刻。這些書單純讀也非常有好處,但是如果可以結合到自己的項目中,會有更大裨益。因爲只有產生了強烈的共鳴,才能保證真正理解了一個東西。
上面提到的一些措施,都是我遇到過的,所以印象比較深刻,這幾本書中還有大量提高程序質量的方法,我這裏只是一個引子,希望給有心人打開一扇窗戶。