2010年5月29日 星期六

養成寫程式的好習慣

我很喜歡 Kent Beck 說的這段話
“I’m not a great programmer, I’m a pretty good programmer with great habits.”
從學生時代開始,我就習慣照著書上的建議寫程式,一直沒覺得什麼特別的。後來和一些人合作,或是看到別人抱怨那裡又出錯了,才驚覺那些好習慣有這麼大的影響力。試著在一些場合向別人說明如何改進寫程式的方式,才發覺很難改變寫程式的習慣 — 大概就和我一直無法早睡早起一樣難...。

大前研一說許多人在解決問題時誤把結果當原因,沒有深入追究問題的根本,只是解決問題根源造成的表面問題。寫程式自然會有 bug,但是有許多 bug 並不是 bug,而是壞習慣造成的。像是在 PHP 或 JavaScript 裡不用 === 和 !==,而被 type casting 誤導造成 bug。或是在 if / while 裡用 assignment 又不小心漏了括號,寫出 if (a=foo() > 0) 這類 bug。
好習慣得隨時間慢慢累積,做得愈多愈久,功力自然會變深。就如同龜仙人要悟空和克林送牛奶那般練基礎功,帶著好習慣寫程式,經年累月下來,學到的東西會更多。以下針對一些特定的習慣提我自己的感受。

Coding style

網路上有不少 coding style 建議,有些如 Google coding style 甚至會解釋為何要這麼寫,這樣寫的好處和壞處為何。可以學到不少寫程式的小技巧。
最近遇到的實例是遵守 Python coding style 所說,在 module 開頭寫 import,並照順序 import 內建、第三方模組和自己的模組。在程式寫到近兩萬行後要做些修改時,我發覺這個簡單的習慣,讓我很容易明白那些模組有關聯,很快就能找出要修改的程式。反過來說,閱讀一些 Django 程式碼時,發覺常在函式裡 import module,不易掌握模組之間的關係。
還有控制函式的行數在螢幕的高度內、區域變數的使用範圍(要用到時再「宣告」)、避免用全域變數。遵守這些習慣使我容易掌握變數的影響範圍,除錯時可以省下不少心思思考變數是否被別的地方改到。
最近初學 JavaScript,就在 coding style 的建議裡找到減少 global object 和管理變數 scope 的技巧:使用一個 global object 存放所有變數和函式,藉此在 JavaScript 中做出模組的效果。

Version control (以 Mercurial 的指令為例)

即使一個人寫程式,version control 仍有很大的用處。保持每個 commit 精簡,每個 commit 只完成一個小功能,就能輕易追踪過去的改變。
以下是幾個我常用 VCS 協助的情況:
  • 寫程式較不怕被中斷,只要 hg diff 就知道剛才改了什麼。commit 前也能清楚明白這次做了那些修改,去掉忘了除掉的 debug code。
  • 可以放心地修改,改到昏頭就 hg up -C 清掉剛才不知所云的修改,不用花費力氣將程式弄回正常的版本。
  • 寫到一半發覺要先完成另一個功能,hg shelve 暫存目前的修改,接著將另一個功能做完並 commit,再 hg unshelve 回頭做原本的事,可以輕鬆地切換目標,隨時專注在目前的目標上。
  • 若發覺某個功能忽然不能運作,hg up 切回舊的版本,做個 binary search (或用 hg bisect) 立即找到改出問題的 commit。由於每個 commit 都很精簡,看一下就會找到改爛的原因。
我最近用 jQuery 寫的程式,過了一陣子後發覺某個功能不能運作。用 hg up 和 binary search 的方式,很快地找到在一百多版前改爛的,而且改爛的原因很奇妙,我將目標 tag 的 id 設為 “submit” 後就爛了,但若換個名字或不設 id 就沒事。若沒有 version control,我想我在原本的程式裡找半天也不會找到,根本不會懷疑問題出在這裡。最後大概會重寫該段,然後莫明奇妙地避開這個問題。
和人合作時 version control 就更有用了,方便和其他人共用程式、做 code review、自動跑測試確保各版運作正常,好處不勝杖舉。相較於每個人各自寫程式,多個人同寫一份程式不但方便討論,容易互相支援,開發時士氣也會較好。每次看到別人 push code,就會覺得待做事項漸漸變少,而寫得更有勁。

Refactoring

在寫新功能或修 bug 前,若發現有重覆程式碼或一些有潛在風險的程式,先重構程式再回頭做原本該做的事。重構前記得要確保重構後行為不變。若情況太糟很難補 unit test 或是沒時間補太細,準備好幾組常用的輸入資料,記好它們對應的輸出結果,寫個自動測試的 recorded test 會比較安全,也可加快後續的重構。配合 VCS 做起來更容易,改改發現無法通過 recorded test,就 hg up -C 重頭改一次。然後別太貪心,一次改一點比較不容易犯錯。視情況寫 recorded test 的方式有所不同。通常我會用 shell script + diff 這類指令很快的拼一個可以用的小工具,重構完就丟了,節省準備 recorded test 的時間還有免除日後維護它的負擔。但若打算長久維護的話,自然是照規矩一步步做會比較穩當。

Unit test / Acceptance test / TDD

這部份在先前的文章已提了不少,隨著時間演進,實例愈來愈多。最近將 Django 1.1 昇到 1.2,跑 unit test OK, 但跑 Selenium test 時看到 CSRF 的錯誤訊息。稍微修一下,測試程式全過。讓我立即確信所有用到的 plugin 都順利地在 1.2 版下運作。
開始用 TDD 可說是我寫程式生涯中的重大里程碑,踏入完全不同的格局。讓我明白如何寫出易於長期發展的程式,不用像在玩踩地雷般辛苦。

Pair programming

這不算是個人習慣,順便記在這裡。
這部份我沒太多經驗,有時運作的不錯,有時不太順。執行 pair programming 前要先確保兩人的背景知識差不多,才不會有一人跟不上進度,讓另一人空轉。運作順利時,可以很快地完成較複雜的設計,並確保至少有兩個人可以繼續維護這份程式。而且程式也會較易懂:兩個人覺得好懂的程式,遠比一個人覺得好懂的程式易懂多了。
Pair programming 比寫下規範更容易讓大家有一致的開發習慣,像是 coding style 或是 commit 的規範。藉由一人帶一人的方式連結開發習慣。也方便分享實作技巧,像操作工具的技巧、使用函式庫的經驗或是寫程式的技巧。

其它

除養成好習慣外,偶而抽點時間學習工具的操作,像是 Linux 架站裝軟體之類的,開發軟體時很難避免這些事。像我習慣用 Linux terminal 開發程式,多熟悉 screen、bash、Vim 的設定和操作,開發速度可以快上不少。
最近的例子是使用 Firefox 的 plugin Firebug。以前改 CSS 都笨笨地存檔、重讀網頁,用 Firebug 讓我用十倍以上的速度完工,令人不勝噓唏。

結語

剩下的就是熟悉函式庫、框架,還有學會資訊工程一些基本知識,了解程式背後運作的原理。一但每個人都能保持寫程式的好習慣,團隊合作將會簡單許多,大家方便共用程式,方便互相支援 (寫相依的元件或除錯),既能加快開發速度,也會比較有趣。