代碼整潔之道

記住沃德原則:“如果每個例程都讓你感到深合己意,那就是整潔代碼。”要遵循這一原則,多半工作都在於爲只做一件事的小函數取個好名字。函數越短小、功能越集中,就越便於取個好名字。

1.不寫重複代碼

2.先讓代碼易讀,才能輕鬆寫代碼。

(1)每個函數都一目瞭然。每個函數都只說一件事。而且,每個函數都依序把你帶到下一個函數。這就是函數應該達到的短小程度!

要判斷函數是否不止做了一件事,就是看是否能再拆出一個函數,該函數不僅只是單純地重新詮釋其實現。

只做一件事的函數無法被合理地切分爲多個區段。

要確保函數只做一件事,函數中的語句都要在同一抽象層級上。

(2)代碼塊和縮進 

public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { 

if (isTestPage(pageData)) 

includeSetupAndTeardownPages(pageData, isSuite); 

return pageData.getHtml(); 

}

if語句、else語句、while語句等,其中的代碼塊應該只有一行。該行大抵應該是一個函數調用語句。這樣不但能保持函數短小,而且,因爲塊內調用的函數擁有較具說明性的名稱,從而增加了文檔上的價值。 

這也意味着函數不應該大到足以容納嵌套結構。所以,函數的縮進層級不該多於一層或兩層。當然,這樣的函數易於閱讀和理解。

(3)自頂向下讀代碼:向下規則。:程序就像是一系列TO起頭的段落,每一段都描述當前抽象層級,並引用位於下一抽象層級的後續TO起頭段落。

3.一個類不要超過200行,一個方法不要超過20行

4.易讀命名。

(1)名副其實,見名知意。變量,函數,參數,類,包的命名要表達它爲什麼存在,它做什麼事,它應該怎麼用。

public List<int[]> getThem() {

 List<int[]> list1 = new ArrayList<int[]>(); 

for (int[] x : theList) 

     if (x[0] == 4) 

     list1.add(x); 

return list1;

 }

上面代碼存在問題:

(1)theList零下標條目的意義是什麼? 

(2)值4的意義是什麼? 

(3)我怎麼使用返回的列表?

零下標條目是一種狀態值,而該種狀態值爲4表示“已標記”.返回的列表標識已經標記的單元格。

public List<int[]> getFlaggedCells() { 

List<int[]> flaggedCells = new ArrayList<int[]>(); 

for (int[] cell : gameBoard) 

   if (cell[STATUS_VALUE] == FLAGGED) 

     flaggedCells.add(cell); 

return flaggedCells; 

}

(2)避免誤導。別用accountList來指稱一組賬號,除非它真的是List類型。

(3)做有意義的區分。

以數字系列命名(a1、a2,„„aN)是依義命名的對立面。這樣的名稱純屬誤導—完全沒有提供正確信息;沒有提供導向作者意圖的線索。試看: 

public static void copyChars(char a1[], char a2[]) { 

for (int i = 0; i < a1.length; i++) 

a2[i] = a1[i]; 

} 如果參數名改爲source和destination,這個函數就會像樣許多。

(4)類名 。

類名和對象名應該是名詞或名詞短語,如Customer、WikiPage、Account和AddressParser。避免使用Manager、Processor、Data或Info這樣的類名。類名不應當是動詞。

(5)方法名 。方法名應當是動詞或動詞短語,如postPayment、deletePage或save。

(6)函數參數。 

最理想的參數數量是零(零參數函數),其次是一(單參數函數),再次是二(雙參數函數),應儘量避免三(三參數函數)。有足夠特殊的理由才能用三個以上參數(多參數函數)—所以無論如何也不要這麼做。

因爲,讀者每次看到它都得要翻譯一遍。參數與函數名處在不同的抽象層級,它要求你瞭解目前並不特別重要的細節。

從測試的角度看,參數甚至更叫人爲難。

輸出參數比輸入參數還要難以理解。讀函數時,我們慣於認爲信息通過參數輸入函數,通過返回值從函數中輸出。我們不太期望信息通過參數輸出。所以,輸出參數往往讓人苦思之後才恍然大悟。

6.1一元函數的普遍形式

轉換形式(有輸入參數,有輸出參數):像在boolean fileExists("MyFile")中那樣。也可能是操作該參數,將其轉換爲其他什麼東西,再輸出之。

例如,InputStream fileOpen("MyFile")把String類型的文件名轉換爲InputStream類型的返回

事件形式(有輸入參數而無輸出參數):程序將函數看作是一個事件,使用該參數修改系統狀態,例如void passwordAttemptFailedNtimes(int attempts)。小心使用這種形式。應該讓讀者很清楚地瞭解它是個事件。謹慎地選用名稱和上下文語境

6.2標識參數 

標識參數醜陋不堪。向函數傳入布爾值簡直就是駭人聽聞的做法。這樣做,方法簽名立刻變得複雜起來,大聲宣佈本函數不止做一件事。如果標識爲true將會這樣做,標識爲false則會那樣做!

5.使用異常替代返回錯誤碼

if (deletePage(page) == E_OK) { 

if (registry.deleteReference(page.name) == E_OK) { 

if (configKeys.deleteKey(page.name.makeKey()) == E_OK){ 

logger.log("page deleted"); 

} else { 

logger.log("configKey not deleted"); } 

} else { 

logger.log("deleteReference from registry failed"); } 

} else { 

logger.log("delete failed"); return E_ERROR;

 } 

另一方面,如果使用異常替代返回錯誤碼,錯誤處理代碼就能從主路徑代碼中分離出來,得到簡化:

try {

 deletePage(page);

 registry.deleteReference(page.name); 

configKeys.deleteKey(page.name.makeKey()); 

} catch (Exception e) { 

logger.log(e.getMessage()); 

}

6.抽離Try/Catch代碼塊

public void delete(Page page) { 

try { deletePageAndAllReferences(page); 

} catch (Exception e) { 

logError(e); } 

}

private void deletePageAndAllReferences(Page page) throws Exception { 

deletePage(page); 

registry.deleteReference(page.name);

 configKeys.deleteKey(page.name.makeKey());

 }

private void logError(Exception e) { 

logger.log(e.getMessage());
}

如何寫出這樣的函數:

寫代碼和寫別的東西很像。在寫論文或文章時,你先想什麼就寫什麼,然後再打磨它。初稿也許粗陋無序,你就斟酌推敲,直至達到你心目中的樣子。 

我寫函數時,一開始都冗長而複雜。有太多縮進和嵌套循環。有過長的參數列表。名稱是隨意取的,也會有重複的代碼。不過我會配上一套單元測試,覆蓋每行醜陋的代碼。

 然後我打磨這些代碼,分解函數、修改名稱、消除重複。我縮短和重新安置方法。有時我還拆散類。同時保持測試通過。

發佈了57 篇原創文章 · 獲贊 33 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章