編程的智慧

最近讀到了一篇很好的關於編程思考的文章,思考之後整理一下,尤其是裏面的一些代碼片段,很有代表性,希望以後回望時仍然有收穫。

原文地址:http://kb.cnblogs.com/page/549080/


編程是創造性的工作,需要靈感和汗水,需要不斷思考和實踐,同時還需要有人指點迷津,以使自己以最優的方式成長,兼具速度和質量。

反覆推敲代碼

提高編程水平最有效的辦法是什麼?是反反覆覆地修改和推敲代碼。

有位文豪說得好:“看一個作家的水平,不是看他發表了多少文字,而要看他的廢紙簍裏扔掉了多少。”同樣的理論適用於編程。好的程序員,他們刪掉的代碼,比留下來的還要多很多。去糟粕,留精華,這是普遍規律。

寫優雅的代碼

優雅的代碼整整齊齊,邏輯清晰,無論是功能分類,還是流程細節,都讓人覺得從容,優雅。
程序所做的幾乎一切事情,都是信息的傳遞和分支。類比電路,電流經過導線,分流或者匯合。如果你是這樣思考的,你的代碼裏就會比較少出現只有一個分支的 if 語句,它看起來就會像這個樣子:

if (...) {
  if (...) {
    ...
  } else {
    ...
  }
} else if (...) {
  ...
} else {
  ...
}

注意到了嗎?在上面的代碼裏面,if 語句幾乎總是有兩個分支。它們有可能嵌套,有多層的縮進,而且 else 分支裏面有可能出現少量重複的代碼。然而這樣的結構,邏輯卻非常嚴密和清晰。

寫模塊化的代碼

模塊化的代碼,不是簡單將功能文件放入不同文件和目錄,也不是強行將不同功能分成不同函數。一個模塊應該像一個電路芯片,有明確的輸入和輸出。實際上一種很好的模塊化方法早已經存在,它的名字叫做“函數”。每一個函數都有明確的輸入(參數)和輸出(返回值),同一個文件裏可以包含多個函數,所以其實根本不需要把代碼分開在多個文件或者目錄裏面,同樣可以完成代碼的模塊化。甚至把代碼全都寫在同一個文件裏,卻仍然是非常模塊化的代碼。

想要做到代碼模塊化,以下幾點很關鍵:

1.避免函數太長。

40-50行即可,一頁屏幕或人眼觀察能力基本就是4、50行,過長的代碼不僅不易讀而且容易造成邏輯混亂。

2.製作小的工具函數。

一些常用的功能會在代碼中反覆使用(如輸出信息到UI、時間統計等等),提煉成小的工具函數有利於效率和邏輯性的提升。

3.每個函數只做一件簡單的事。

有些人喜歡製造一些“通用”的函數,既可以做這個又可以做那個,它的內部依據某些變量和條件,來“選擇”這個函數所要做的事情。比如,你也許寫出這樣的函數(注意,很多人願意這樣寫):

void foo () {
  if (getOS () .equals ("MacOS")) {
    a ();
  } else {
    b ();
  }
  c ();
  if (getOS () .equals ("MacOS")) {
    d ();
  } else {
    e ();
  }
}


<span style="font-weight: normal; font-size: 14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">寫這個函數的人,根據系統是否爲“MacOS”來做不同的事情。你可以看出這個函數裏,其實只有c()是兩種系統共有的,而其它的a(), b(), d(),e()都屬於不同的分支。</span>
  這種“複用”其實是有害的。如果一個函數可能做兩種事情,它們之間共同點少於它們的不同點,那你最好就寫兩個不同的函數,否則這個函數的邏輯就不會很清晰,容易出現錯誤。其實,上面這個函數可以改寫成兩個函數:

void fooMacOS () {
  a ();
  c ();
  d ();
}

void fooOther () {
  b ();
  c ();
  e ();
}


如果你發現兩件事情大部分內容相同,只有少數不同,多半時候你可以把相同的部分提取出去,做成一個輔助函數。比如,如果你有個函數是這樣:

void foo () {
  a ();
  b ()
  c ();
  if (getOS () .equals ("MacOS")) {
    d ();
  } else {
    e ();
  }
}


其中a(),b(),c()都是一樣的,只有d()和e()根據系統有所不同。那麼你可以把a(),b(),c()提取出去:

void preFoo () {
  a ();
  b ()
  c ();
}

然後製造兩個函數:
<span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></span><pre name="code" class="cpp">void fooMacOS () {
  preFoo ();
  d ();
}



<span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">和</span>
<span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></span><pre name="code" class="cpp">void fooOther () {
  preFoo ();
  e ();
}



<span style="font-weight: normal; font-size: 14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">這樣一來,我們既共享了代碼,又做到了每個函數只做一件簡單的事情。這樣的代碼,邏輯就更加清晰。</span>

4.避免使用全局變量和類成員(class member)來傳遞信息,儘量使用局部變量和參數。

有些人寫代碼,經常用類成員來傳遞信息,就像這樣(本人之前一直這麼用難過):

class A {
   String x;

   void findX () {
      ...
      x = ...;
   }

   void foo () {
     findX ();
     ...
     print (x);
   }
 }


首先,他使用findX(),把一個值寫入成員x。然後,使用x的值。這樣,x就變成了findX和print之間的數據通道。由於x屬於class A,這樣程序就失去了模塊化的結構。由於這兩個函數依賴於成員x,它們不再有明確的輸入和輸出,而是依賴全局的數據。findX和foo不再能夠離開class A而存在,而且由於類成員還有可能被其他代碼改變,代碼變得難以理解,難以確保正確性。
  如果你使用局部變量而不是類成員來傳遞信息,那麼這兩個函數就不需要依賴於某一個 class,而且更加容易理解,不易出錯:

String findX () {
    ...
    x = ...;
    return x;
 }
 void foo () {
   int x = findX ();
   print (x);
 }


寫可讀的代碼

說到可讀的代碼,很多人第一反應就是註釋,有很多編程規範要求註釋量要達到代碼總量的30%甚至更高,當然這個問題衆說紛紜,但個人認爲,良好的代碼風格比添加註釋更能說明一段代碼的功能和含義,過多的註釋不僅破壞代碼完整性,而且一旦代碼修改,很多註釋會失效,註釋的添加和修改成爲很多程序員不願觸碰之殤。
註釋常用在以下典型位置:

  1.說明主要流程時

  2.在違反常規思維的設計時

  3.在值得留意或預留功能時

同時,以下方法可以幫助你減少註釋量的同時維持程序可讀性:

1.使用有意義的函數和變量名字。

如果你的函數和變量的名字,能夠切實的描述它們的邏輯,那麼你就不需要寫註釋來解釋。比如:

// put elephant1 into fridge2
put (elephant1, fridge2);


2.局部變量應該儘量接近使用它的地方。

有些人喜歡在函數最開頭定義很多局部變量,然後在下面很遠的地方使用它,其實可以挪到接近使用它的地方:就像這個樣子:

void foo () {
  ...
  ...
  int index = ...;
  bar (index);
  ...
}


這樣讀者看到bar (index),不需要向上看很遠就能發現index是如何算出來的。而且這種短距離,可以加強讀者對於這裏的“計算順序”的理解。否則如果 index 在頂上,讀者可能會懷疑,它其實保存了某種會變化的數據,或者它後來又被修改過。如果 index 放在下面,讀者就清楚的知道,index 並不是保存了什麼可變的值,而且它算出來之後就沒變過。
  如果你看透了局部變量的本質——它們就是電路里的導線,那你就能更好的理解近距離的好處。變量定義離用的地方越近,導線的長度就越短。你不需要摸着一根導線,繞來繞去找很遠,就能發現接收它的端口,這樣的電路就更容易理解。
3.局部變量名字應該簡短。
這貌似跟第一點相沖突,簡短的變量名怎麼可能有意義呢?注意我這裏說的是局部變量,因爲它們處於局部,再加上第 2 點已經把它放到離使用位置儘量近的地方,所以根據上下文你就會容易知道它的意思:

boolean success = deleteFile ("foo.txt");
if (success) {
  ...
} else {
  ...
}


4.不要重用局部變量。

以下是一個重用的反例:

String msg;
if (...) {
  msg = "succeed";
  log.info (msg);
} else {
  msg = "failed";
  log.info (msg);
}


從讀者心裏來講,看見msg被多次賦值,會思考msg有沒有在其他地方賦值,這裏用它準備嗎等等之類的懷疑。簡單改成這樣會好得多:

if (...) {
  String msg = "succeed";
  log.info (msg);
} else {
  String msg = "failed";
  log.info (msg);
}


5.把複雜的邏輯提取出去,做成“幫助函數”。

有些人寫的函數很長,以至於看不清楚裏面的語句在幹什麼,所以他們誤以爲需要寫註釋。如果你仔細觀察這些代碼,就會發現不清晰的那片代碼,往往可以被提取出去,做成一個函數,然後在原來的地方調用。由於函數有一個名字,這樣你就可以使用有意義的函數名來代替註釋。舉一個例子:

...
// put elephant1 into fridge2
openDoor (fridge2);
if (elephant1.alive ()) {
  ...
} else {
   ...
}
closeDoor (fridge2);
...


如果你把這片代碼提出去定義成一個函數:

void put (Elephant elephant, Fridge fridge) {
  openDoor (fridge);
  if (elephant.alive ()) {
    ...
  } else {
     ...
  }
  closeDoor (fridge);
}


這樣原來的代碼就可以改成:

...
put (elephant1, fridge2);
...


更加清晰,註釋也沒必要了。
6.把複雜的表達式提取出去,做成中間變量。

Crust crust = crust (salt (), butter ());
Topping topping = topping (onion (), tomato (), sausage ());
Pizza pizza = makePizza (crust, topping);


7.在合理的地方換行。

if (someLongCondition1() && 
       someLongCondition2() && 
       someLongCondition3() && 
       someLongCondition4()) {
     ...
   }



寫簡單的代碼

簡單並不代表省略,以下幾條建議會幫助你避免因爲追求簡單而犯錯:

1.永遠不要省略花括號{}

2.合理使用括號(),不盲目依賴操作符優先級

3.避免使用continue和break

第3條很多人會有疑問,我也思考了一陣,個人認爲這是個仁者見仁智者見智的問題,從原文的角度考慮,continue和break是破壞程序順序執行的額外加入的強邏輯手段,可以考慮這樣改寫:

1)如果出現了 continue,你往往只需要把 continue 的條件反向,就可以消除 continue。

2)如果出現了 break,你往往可以把 break 的條件,合併到循環頭部的終止條件裏,從而去掉 break。(這個有點牽強)
3)有時候你可以把 break 替換成 return,從而去掉 break。
4)如果以上都失敗了,你也許可以把循環裏面複雜的部分提取出來,做成函數調用,之後 continue 或者 break 就可以去掉了。

對應的舉例我就省略了,有興趣的可以看原文

文中還提及如何處理錯誤、如何處理NULL指針等等,後續我另寫文章來總結。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章