代碼簡潔之道 - 筆記

1. 什么是整潔代碼

我喜歡優雅和高效的代碼。代碼邏輯應當直截了當,叫缺陷難以隱藏;盡量減少依賴關系,使之便于維護;依據某些分層戰略完善錯誤處理代碼;性能調至最優,省得引誘別人做沒規矩的優化,搞出一堆混亂來。整潔的代碼只做好一件事情。 --Bjarne Stroustrup,C++語言發明者,C++ Programming Language(中譯版《C++程序設計語言》)一書作者

整潔的代碼簡單直接。整潔的代碼如同優美的散文。整潔的代碼從不隱藏設計者的意圖,充滿了干凈利落的抽象和直接了當的控制語句。 --Grady Booch,Object Oriented Analysis and Design with Applications(中譯版《面向對象分析與設計》)一書作者

整潔的代碼應可由作者之外的開發者閱讀和增補。 它應有單元測試和驗收測試。它使用又意義的命名。它只提供一種而非多種做一件事的途徑。它只有盡量少的依賴關系, 而且要明確地定義和提供清晰、盡量少的API。代碼應通過其字面表達含義,因為不同的語言導致并非所有必需信息均可通過代碼自身清晰表達。--“老大”Dave Thomas,OTI公司創始人,Eclipse戰略教父

我可以列出我留意到的整潔代碼的所有特點,但其中有一條是根本性的。整潔的代碼總是看起來像是某位特別在意它的人寫的。幾乎沒有改進的余地。代碼作者什么都想到了,如果你企圖改進它,總會回到原點,贊嘆某人留給你的代碼——全心投入的某人留下的代碼。 --Michael Feathers,Working Effectively with Legacy Code(中譯版《修改代碼的藝術》)一書作者。--

近年來,我開始研究貝克的簡單代碼規則,差不多也都琢磨透了。簡單代碼,依其重要順序:

  • 能通過所有測試
  • 沒有重復代碼
  • 體現系統中的全部設計理念
  • 包括盡量少的實體, 比如類、方法、函數等。

在以上諸項中,我最在意代碼重復。如果同一段代碼反復出現,就表示某種想法未在代碼中得到良好的體現。我盡力去找出到底那是什么,然后再盡力更清晰地表達出來。

在我看來,有意義的命名是體現表達力的一種方式,我往往會修改好幾次才會定下名字來。借助Eclipse這樣的現代編碼工具,重命名代價極低,所以我無所顧忌。然而,表達力還不只體現在命名上。我也會檢查對象或方法是否想做的事太多。如果對象功能太多,最好是切分為兩個或多個對象。如果方法功能太多,我總是使用抽取手段(Extract Method)重構之,從而得到一個能較為清晰地說明自身功能的方法,以及另外數個說明如何實現這些功能的方法。

消除重復和提高表達力讓我在整潔代碼方面獲益良多,只要銘記這兩點,改進臟代碼時就會大有不同。不過,我時常關注的另一規則就不太好解釋了。

這么多年下來,我發現所有程序都由極為相似的元素構成。例如“在集合中查找某物”。不管是雇員記錄數據庫還是名-值對哈希表,或者某類條目的數組,我們都會發現自己想要從集合中找到某一特定條目。一旦出現這種情況,我通常會把實現手段封裝到更抽象的方法或類中。這樣做好處多多。

可以先用某種簡單的手段,比如哈希表來實現這一功能,由于對搜索功能的引用指向了我那個小小的抽象,就能隨需應變,修改實現手段。這樣就既能快速前進,又能為未來的修改預留余地。

另外,該集合抽象常常提醒我留意“真正”在發生的事,避免隨意實現集合行為,因為我真正需要的不過是某種簡單的查找手段。

減少重復代碼,提高表達力,提早構建簡單抽象。這就是我寫整潔代碼的方法。

--Ron Jeffries,Extreme Programming Installed(中譯版《極限編程實施》)以及Extreme Programming Adventures in C#(中譯版《C#極限編程探險》)作者。

Ron以寥寥數段文字概括了本書的全部內容。不要重復代碼,只做一件事,表達力,小規模抽象。該有的都有了。

如果每個例程都讓你感到深合己意,那就是整潔代碼。如果代碼讓編程語言看起來像是專為解決那個問題而存在,就可以稱之為漂亮的代碼。 --Ward Cunningham,Wiki發明者,eXtreme Programming(極限編程)的創始人之一,Smalltalk語言和面向對象的思想領袖。所有在意代碼者的教父--

編程中的讀寫比例

在寫新代碼的時候, 我們會一直讀舊代碼。 讀與寫花費時間的比例超過10:1!既然比例如此之高, 我們想讓讀的過程變得輕松, 即便那會使得編寫過程更難。沒有可能光寫不讀,所以使之易讀實際也使之易寫。

童子軍軍規

讓營地比你來時更干凈。
如果每次嵌入時,代碼都比簽出時干凈,那么代碼就不會腐壞。 清理并不一定要花多少功夫, 也許只是改好一個變量名, 拆分一個有點過長的函數, 消除一點點重復代碼, 清理一個嵌套的if語句。
持續改進是專業性的內在組成部分。

2. 有意義的命名

名副其實

bad:

int d; // 消逝的時間,以日記

good:

int elapsedTimeInDays;

用一個名副其實的函數,掩蓋住魔術數,例如:
good:

if ( cell.IsFlagged() )

bad:

if (cell[STATUS_VALUE] == FLAGGED)

避免誤導

程序員必須避免留下掩藏代碼本意的錯誤線索。
別用accountList來指定一組賬號,除非它真的是List類型。 用accountGroup或bunchOfAccounts, 甚至直接用accounts都會好一些。

做有意義的區分

good:

public static void copyChars(char source[], char destination[]) {
    for (int i = 0; i < source.length; i++ ) {
         destination[i] = source[i];
    }
}

bad:

public static void copyChars(char a1[], char a2[]) {
    for (int i = 0; i < a1.length; i++ ) {
         a2[i] = a1[i];
    }
}

廢話是另一種沒有意義的區分。 假設你有一個Product類型。 如果還有一個ProductInfo 或 ProductData類, 那它們的名稱雖然不同, 意思卻無區別。Info 和 Data 就像 a、an和 the 一樣。
bad:

getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
// 程序員怎么能知道該調用哪個函數呢。

要區分名稱,就要以讀者能鑒別不同之處的方式來區分。

使用讀得出來的名稱

不要使用非恰當的英語詞(不用讀出傻乎乎的自造詞)
bad:

class DtaRcrd102 {
    private Date genymdhms;
    private Date modymdhms;
    private final String pszqpint = "102";
    /* ... */
};

good:

class Customer {
    private Date generationTimeStamp;
    private Date modificationTimestamp;
    private final String recordId = "102";
    /* ... */
}

使用可搜索的名稱

bad:

for (int j=0; j<34; j++) {
   s += (t[j] * 4)/5;
}

good:

int realDaysRerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for ( int j=0; j < NUMBER_OF_TASKS; j++) {
    int realTaskDays =  taskEstimatej] * realDaysRerIdealDay ;
    int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK );
    sum += realTaskWeeks;
}

成員前綴

也不必使用 m_ 前綴來標明成員變量。應當把類和函數做得足夠小,消除對成員前綴的需要。

public class Part {
    private String  m_desc; // The testual description
    void setName(String name) {
        m_desc = name;
    }
}
----------------------------------
public class Part{
    String description;
    void setDescription(String descripthion) {
        this.description = description;
    }
}

人們會很快學會無視前綴(或后綴), 只看到名稱中有意義的部分。 代碼讀得越多,眼中就越沒有前綴。

接口和實現

如果你在做一個創建形狀用的抽象工廠(Abstract Factory)。 該工廠是個接口(Interface), 要用具體類來實現。 你怎么命名工廠和具體類呢?

如何寫出好的函數

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

我寫函數時,一開始都冗長而復雜。有太多縮進和嵌套循環。有過長的參數列表。名稱時隨意取的,也會有重復的代碼。不過我會配上一套單元測試,覆蓋每行丑陋的代碼。 然后我打磨這些代碼,分解函數、修改名稱、消除重復。我縮短和重新安置方法。有時我還拆散類。同時保持測試通過。

最后,遵循本章列出的規則,我組裝好這些函數。我并不從一開始就按照規則寫函數。我想沒人做得到。

函數
1、短小 < 15行
函數的第一規則是短小,第二規則是更短小。每個函數都只說一件事,每個函數都一目了然。每個函數都依序把你帶到下一個函數。

2、代碼塊和縮進(不超過三層)
if else while語句等,其中的代碼塊應該只有一行,即函數調用語句,以保持函數短小。

3、只做一件事
函數應該只做一件事,做好這件事。只做這一件事。判斷函數是否不止做了一件事的方法,看是否能再抽出一個函數。函數中如果存在多個區段,表明函數做的事情不止一件。

4、每個函數一個抽像層級。
每個函數一個抽像層級。確保函數只做一件事,函數中所有的語句都在一個抽象層級上。函數中混雜不同抽象層級,往往讓人迷惑,更惡劣的是,就像破損的窗戶,一旦細節與基礎概念混雜,更多的細節就會在函數中糾結起來。

5、自頂向下閱讀代碼:向下規則
讓每個函數后面都跟著位于下一抽象層級的函數,在查看函數列表時,就能依抽象層級向下閱讀。

6、確保每個switch都埋藏在較低的抽象層級,而且永遠不重復
7、使用描述性的名稱
使用描述性的名稱:函數越短小,功能越集中,就越便于取個好名字
長而具有描述性的名稱,比短而令人費解的名稱好。長而具有描述性的名稱,要比描述性的長注釋好。讓函數名稱中的多個單詞容易閱讀,然后使用這些單詞給函數取個能說清其功用的名稱。別害怕花時間取名字,嘗試不同的名稱,實測閱讀效果。選擇描述性的名稱能理清你關于模塊的設計思路,并幫你改進。追索好名稱,往往導致對代碼的改善重構。命名方式要保持一致。使用與模塊名一脈相承的短語、名詞和動詞給函數命名。使用類似措辭,依序講出一個故事。

8、函數參數,最好沒有,盡量避免三個。
足夠特殊理由才能用三個以上參數。從測試角度看,參數更叫人為難。

9、標識參數 ,這是標識函數不止做一件事情的明確信號,應該拆分。
10、二元函數,盡量改寫為一元函數
利用一些機制將其轉換為一元函數。(多個參數封裝成臨時類,不是一個好的解決方式)

11、函數和參數應當形成一種非常良好的動/、名詞對形式
如assertEqual 改成assertExpectedEqualsActual(expected,actual)大大減輕記憶參數順序的負擔。

12避免使用輸出參數
如果函數必須要修改某種狀態,就修改所屬對象的狀態。

13、分隔指令與詢問
函數要么做什么事 要么回答什么事,但二者不可得兼。
函數應該修改某對象的狀態,或是返回該對象的有關信息。兩樣都干常會導致混亂。

14、使用異常替代返回錯誤碼。
從指令式函數返回錯誤碼輕微違反了指令與詢問分隔的規則。鼓勵了在if語句判斷中把指令當做表達式使用。另一方面,如果使用異常替代返回錯誤碼,錯誤處理代碼就能從主路徑徑代碼中分離出來,得到簡化。

15、抽離Try/Catch代碼塊。
Try/Catch代碼丑陋不堪。他們搞亂了代碼結構,把錯誤處理與正常流程混為一談。把try和catch代碼塊的主體部分抽離出來,另外形成函數。

函數應該只做一件事。錯誤處理就是一件事。因此,處理錯誤的函數不應該做其他的事,如果關鍵字try在某個函數中存在,它就應該是這個函數的第一個單詞,而且在catch/fianlly代碼塊后面也不應該有其他內容。

16、別重復自己
重復可能是軟件中一切邪惡的根源。許多原則與實踐規則都是為控制與消除重復而創建。如面向對象將代碼集中到積累,避免冗余。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容