作為一個程序員,我們必須時刻想着兩個問題:1,什么樣的代碼可以謂之為優秀?2,怎樣才能寫出優秀的代碼?
一段優秀的代碼,它一般需要滿足以下幾個條件:
#統一規范#
所有的代碼,第一前提必須是統一規范,而常見的統一規范主要包括有以下內容:
1)統一編輯器規范
在團隊開發中,我們並不對各個開發人員使用的編輯器做硬性要求,你可以使用常見的如Eclipse、WebStrom、Sublime等集成開發環境IDE,也可以使用UltraEdit這種,甚至Vim都無所謂。但是我們還是會做一些統一規范要求,比如Tab鍵統一使用空格Space替換(一個Tab為四個Space)、文件編碼統一為UTF-8、換行的Line Delimiter統一為Unix的LF而不是Windows的CRLF(PHP代碼尤其重視這點)等等。
2)統一代碼風格
我們知道代碼的風格有很多種,尤其是在涉及到括號的使用上,比如:
- K&R括號風格
K&R風格是最早為人們所喜愛的放個,它是由C語言之父Kernighan和Ritchie在他們的《C程序設計語言》一書中確立的,它也常被認為是最初和最好的風格,它可以在一個小屏幕中盡可能顯示更多的信息。這個也是我個人最常用的風格。
int k_and_r() { int a = 0, b = 0; while (a != 10) { a++; b++; } return b; }
- 懸掛式括號風格
懸掛式的風格在空間上顯示上更加開闊,由於有着更明顯的前括號,也使得代碼更加易於瀏覽;但在豎向空間上占用更多。
int exdented() { int a = 0, b = 0; while (a != 10) { a++; b++; } return b; }
- 縮進的括號風格
縮進風格並不太常見,在這種風格下括號隨代碼一起縮進,這種風格也被稱為“Whitesmith”風格,因為早期的Whitesmith的C編譯器的示例代碼使用的就是這種風格,個人並不推薦。
int indented() { int a = 0, b = 0; while (a != 10) { a++; b++; } return b; }
- 其他風格
還有一些其他的括號風格,比如GNU風格是介於懸掛式和縮進式風格之間的一種風格,括號被放置在各個縮進級別的一半的位置
3)統一命名規范
命名包括文件的命名、類的命名、方法的命名、變量的命名等,良好的命名使得代碼易於閱讀,也更加易於維護。命名的方法有很多,常見的的有:
- 匈牙利命名法
匈牙利命名法是一種有爭議的命名約束,它將關於變量或函數的類型的信息編入它們的名稱當中,要求開頭字母使用變量類型的縮寫,其余部分用變量的英文或中文的縮寫,同時要求單詞的第一個字母大寫。這種命名法最初是在20世紀80年代的Microsoft公司中出現的,並在該公司的Win32 API和MFC庫中得到了廣泛的使用,也因此導致了一定的流行性。之所以被稱為“匈牙利命名法”,是因為它的創始人Charles Simonyi是匈牙利人。此外,變量名看起來像是使用匈牙利語書寫的,但是要理解它並不容易,很多非Windows的程序員都會被比如lpszFile、rdParam和hwndItem等的奇怪名字給搞糊塗。
int iMyAge; // "i"是int類型的縮寫 char cMyName[0]; // "c"是char類型的縮寫
- 駝峰式命名法
有的時候又稱為“小駝峰命名法”,它在Java語言庫以及很多C++代碼庫中得到了廣泛使用,這種命名主要源於其大寫字母的布局很像駱駝的駝峰,它規定第一個單詞字母小寫,后面其他的單詞首字母大寫。
int myAge; char myName[0];
- 帕斯卡命名法
這種命名法跟上面的駝峰命名法很相像,唯一區別就是其第一個字母也大寫,所以有時又稱為“大駝峰式命名法”。
int MyAge; char MyName[0];
- 下划線命名法
這種風格在C++標准庫和GNU Foundation中比較常見,也即用下划線來隔開不同的單詞。
int my_age; int my_name[0];
事實上,采用什么樣的命名方式都是可以的(雖然我個人更偏向於駝峰命名法),更重要的是命名必須清晰,比如函數名可以采用動名詞+靜名詞的組合來命名,而不是用foo和bar這種古怪的名稱。另外,建議在清晰的基礎上保持簡潔,否則也很可能出現類似someTypeWithMeaningfulNaming這種非常冗余的命名,比如在for循環中使用i而不是使用index就是一種推薦做法。
#簡潔清晰#
什么叫做簡潔?能夠一句代碼解決的事情,就不要寫成兩句代碼。
什么叫做清晰?雖然一句代碼能夠解決事情,但我們有的時候卻將其拆成了多句代碼,使得其變得可能有點冗余。
優先程度上,清晰>簡潔。
每個人寫的代碼,它的后續維護者可能是一個初級程序員,如果他不能理解你的代碼邏輯,或者說你的代碼邏輯很難理解,那么他就有可能會犯一些錯誤。復雜的結構和不常用的語言技巧雖然可以證明你在運算符優先級方面有着熟練經驗,但是這些實際上會扼殺代碼的可維護性。比如下面兩段代碼:
int a = b = c = 10; int result = a * b + b * c - a + b / c; // 下面的代碼雖然不如上面代碼簡潔,但是更加的簡單 int a = 10, b = 10, c = 10; int result = (a * b) + (b * c) - a + (b / c);
清晰的代碼,也往往意味着簡單的代碼。如果對於某段代碼不是基於性能上的需要而寫得復雜,那么,請保持代碼的簡單清晰。
#必要注釋#
作為一個負責任的程序員,我們有義務給我們的代碼寫注釋,即使在編碼任務再繁重的情況下。
但我們也需要注意的是,注釋不是越多越好,我們更加重視注釋的質量,而不是數量。很多時候,一個好的命名已經可以能夠幫我們省下很多注釋。比如下面的代碼:
for (int i = 0; i < wlst.size(); i++) k(wlst[i]); // 上面代碼改成下面代碼 for (int i = 0; i < widgets.size(); i++) { printWidget(widgets[i]); }
注釋中應該包含的內容包括有:
- 解釋為什么,而不是怎么樣
注釋不應該描述代碼是怎樣運行的,這完全可以通過閱讀代碼來了解,你更加應該注意的是描述為什么有些東西要這么寫,比如下面的代碼的注釋就不是必須的,完全可以去掉:
// 循環遍歷所有的widget for (int i = 0; i < widgets.size(); i++) { // 打印這些widget printWidget(widgets[i]); }
- 不要描述代碼
不要試着去描述代碼寫你的代碼都干了什么,更應該要寫的是你的代碼為什么要這樣寫。比如下面的代碼的注釋完全是無效的:
// index自增(我們這里需要寫的注釋更應該是為什么要讓index自增) ++index;
- 不要取代代碼
不要試圖在注釋中去說明某個代碼的限制條件,更應該地使用代碼本身的機制去實現。還有,當有的時候你發現可能要花大量的注釋來描述某段代碼的功能的時候,更好的做法是用代碼去描述,比如把一大段的代碼拆分成多個子函數,給每個子函數賦予更合適的命名等。比如:
// 下面這個方法不允許被類以外訪問 public void getMyAge() { } // 完全可以不要上面的注釋,而使用如下的代碼實現 private void getMyAge() { }
- 避免給代碼造成分心
注釋不應該給本身的代碼造成分心,比如有些程序員喜歡在if的結束加上// end if (a < 1) 這樣的注釋,而這種注釋則是完全沒必要的,只會給原本閱讀代碼造成分心,更合適的做法是通過正確合適的縮進方式等來保證代碼可讀性。
#健壯安全#
我們在編寫代碼的時候,需要考慮各種方方面面的因素,提高代碼的健壯性。比如,在編寫最常見的登錄代碼時,就需要考慮到用戶輸入的用戶名和密碼的多種情況,比如:
public void checkLogin(String username, String password) { if (username == null || password == null) { // Error } if (username.trim().equals('') || password.trim().equals('')) { // Error } // do something }
不能只是簡單地檢查用戶名和密碼是否匹配,還必須要考慮到當用戶名或密碼為空時候的處理邏輯,如果用戶名是手機號,還必須使用正則表達式來檢查所輸入的用戶名是否符合手機號碼樣式等等。
不僅於此,對於安全性上的要求,還必須要對用戶輸入的字符進行處理,防止用戶輸入類似<script>alert()</script>的XSS攻擊代碼和類似'or'1'='1的SQL注入攻擊代碼等。
總之,要編寫健壯安全的代碼,必須時刻考慮到:
- 使用“防御性編程”
“防御性編程”中心原則是“不做設想”,也即永遠不要設想用戶會按照我們寫代碼的預期或要求來使用代碼。一些簡單的防御性規則,比如“檢查所有的輸入”和“驗證所有的運算”,可以幫我們把代碼中許多的安全隱患給消除掉。
- 代碼審核
初級開發人員的代碼在早期必須經由高級開發人員進行代碼審核,有經驗的高級開發人員能夠通過閱讀代碼就能發現很多問題,比如邊界值的判斷上,數組是否會造成溢出和字段長度是否超出數據庫限制等這些都是可以通過代碼審核來發現。
- 嚴格地執行測試和調試,盡可能地消除Bug。
一些敏捷開發的TDD測試驅動開發就是這么做的,它要求代碼必須是可以被測試的,並且這些可以被測試的代碼時刻能夠通過測試樣例。
#高效性能#
隨着現在處理器的越來越強大,很多時候應用的瓶頸並不在代碼而在其他地方(主要是I/O操作,比如讀寫文件和數據庫等),個人更不推薦為了所謂的性能而犧牲了代碼的可讀性和可維護性,除非這塊代碼確實是必須優化的。
但我們也不能仗着處理器的強大就可以完全不顧代碼的性能,有些簡單的優化原則在我們寫代碼的時候就需要時刻遵循。
- 將工作推遲到必須時再做
如果不是馬上就要使用某個文件,那么就不要打開它;如果暫時不需要某個值,那就不要去計算它;如果沒有某個函數程序也可以運行,那就不要去調用它。
- 在函數中做進一步檢查以避免多余的工作
如果一個可能會導致函數計算無效的條件,那么對這個條件的判斷最好放在頂部,防止做了過多無用工作,比如:
public int calculate(int number) { if (number == 0) { return 0; } // do something }
- 將不變條件的計算移到循環外
由於循環中是每次都要做的,那么每次循環中如果某個值都保持不變,則將該值移到循環外面,比如:
for (int i = 0; i < tree.appleCount(); i++) { } // 上面代碼可以改成如下 int appleCount = tree.appleCount(); for (int i = 0; i < appleCount; i++) { // do something }
- 利用“短路求值法”
確保將可能失敗的測試放在最前面以節省時間,比如if (condition_one && condition_two),確保condition_one不為真的可能性比condition_two更大。
- 不要重復進行相同的工作
比如將公共的代碼提取到共享函數中以避免重復計算,或者將某個經常被用到的需要被計算出來的值放到緩存中以備使用。
最后,各種不同的編程語言都有其不同的優化之處,比如Java中使用增強式foreach循環就比傳統的for循環更加高效,具體到各種語言再具體分析。
#易於擴展#
首先,我們對於程序的擴展性判斷,並不是需要其能支持未來需求的變更的可能性。相反,我們不提倡對代碼做“過度設計”,代碼的可擴展性更主要的是體現在未來能夠對需求變化快速響應上,能夠跟隨需求的變化而快速變化,而不是一味不變地支持需求的變化。
但是這也肯定也不能成為我們偷懶的借口,我們還是可以使用一些常規的原則來增強代碼的可擴展性,尤其是時刻保證代碼的“低耦合”非常重要。
- 動態按需加載
不同的功能拆分出來,而不是在一個函數里完成,比如:
public void init() { // load database // init environment // init ui // do something } // 上面的方法把所有的事情都放在一起,擴展性很差,我們需要把不同的功能拆分成不同的函數 public void loadDatabase() {} public void initEnvironment() {} public void initUi() {} public void init() { loadDatabase(); initEnvironment(); initUi(); }
對於配置文件也是一樣,我們需要在程序的入口處能夠根據當前環境不同而加載不同的配置文件,比如Web應用中常見的做法是將本地環境、測試環境、正式環境分成三個配置文件,然后根據當前請求域名的不同而加載不同的文件,而不是把所有的配置寫到一個文件里。
- 抽象接口
對於一些功能類似的,完全可以抽象出一個抽象類,在這個抽象類中定義功能接口,然后再在不同的子類中實現該接口的不同功能,比如Web應用中一種常見的做法是在抽象控制類中定義GET/POST/PUT/DELETE請求協議的接口方法,然后再在不同子類中分別實現不同的功能細節。
public abstract class BaseController { public void get(); public void post(); public void put(); public void delete(); }
#寫在最后#
上面寫了這么多,其實說白了提高寫代碼能力唯一的途徑就是多寫代碼,多思考。對於初級程序員來說,把代碼寫得具有可維護性(規范/簡潔/注釋)是最基本的要求,而對於有經驗的程序員來說,還必須考慮代碼的健壯性、安全性、高效性和可擴展性等。寫代碼的時候多考慮下這些東西,經常重構自己的代碼,就離寫出優秀的代碼不遠了。
最后,給大家推薦幾本個人覺得不錯的書籍,這幾本書對我的代碼生涯有着極大的影響。
1、《編程匠藝 編寫卓越的代碼》,電子工業出版社,Pete Goodliffe(著),韓江, 陳玉(譯);
2、《重構 改善既有代碼的設計》,人民郵電出版社,Martin Fowler(著),熊節(譯);
3、《大話設計模式》,清華大學出版社,程傑(著);
4、《Java優化編程(第2版)》,電子工業出版社,林勝利, 王坤茹(編著);