記住沃德原則:“如果每個例程都讓你感到深合己意,那就是整潔代碼。”要遵循這一原則,多半工作都在於爲只做一件事的小函數取個好名字。函數越短小、功能越集中,就越便於取個好名字。
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());
}
如何寫出這樣的函數:
寫代碼和寫別的東西很像。在寫論文或文章時,你先想什麼就寫什麼,然後再打磨它。初稿也許粗陋無序,你就斟酌推敲,直至達到你心目中的樣子。
我寫函數時,一開始都冗長而複雜。有太多縮進和嵌套循環。有過長的參數列表。名稱是隨意取的,也會有重複的代碼。不過我會配上一套單元測試,覆蓋每行醜陋的代碼。
然後我打磨這些代碼,分解函數、修改名稱、消除重複。我縮短和重新安置方法。有時我還拆散類。同時保持測試通過。