這是我獨力寫過最大的專案了,從八月初開始寫,不知不覺寫到破萬行啊,而且其中有四千多行是測試碼,貫徹當初 TDD 到底的決心。
不得不說,幸虧有用 TDD,不然測試碼大概會丟三落四的,愈後面會愈痛苦,最後就會想砍掉重練。開發過程裡遇過幾次很難改的情況(第一次發生在寫到兩千多行時),幸好仗著測試碼夠齊全,將相關程式重構後,不知不覺就把新功能寫好了。
有時沒啥想法,不知怎麼改較好,就想說「總之就先重構,船到橋頭自然直」。結果這招矇對的機率還滿高的,有時還會發覺比原本構想更簡單的做法。有些人認為重構成本很高,且短期沒有產值,似乎不適合常做。事實上,在測試碼充沛且時常重構的情況下,可以三兩下完成重構,再加上版本管理系統的輔助,重構失敗立即重來,十分方便。
使用 TDD 偶而會有意外收獲,發覺巧妙設計。有時即使看個數次,還是學不會自己完成目標用上的新技巧,感覺好像用外掛過關,卻不知道自己怎麼過的。最後只能期望下回遇到類似的處理,TDD 仍能導引出良好的設計。對照《物件導向程式的九個體操練習》來看,發現在 TDD 的導引之下,自然地會做出較佳的設計(因為較差的設計不好測),而且會因地制宜,做出貼近需求的做法,不會 overdesign。
雖說在很久以前就確定 TDD 是正確的路,也領悟出可測性 (testability) 是首要之務,為了可測性而改變原本的設計也是合適的決定。還是需要許多實際經驗,才能更充份地體會這些原則帶來的影響。我喜歡 freedom 說過的一句話「 system design 一直都是 trade-off」,經過這一年多的歷練 [1],我更肯定 TDD 帶來的 trade-off 是值得的,加強寫測試碼的技術絕對是值回票價的投資。
咦?這篇好像離題成行銷 TDD,回頭來補講 Python 的心得。寫這個專案的途中接觸到比較深的 Python 議題,像是用 ctypes 包 C/C++ API 很方便;好用的畫圖函式庫 matplotlib;還有一拖拉庫的速度問題讓我學了不少,比方說:
- cProfile 絕對是找瓶頸的頭號幫手,做 profiling 時也不會拖慢多少。除演算法的考量外,別自己亂猜浪費時間。
- list 很大但不需用到 sublist 或 index 的功能時,可以用 generator 改寫。
- 若有個欄位被存取數百萬次以上,用 data field 會比用 method 快上不少,因為 Python 的 function call 不快。
- collections.defaultdict 比 dict.get(key, default_value) 快上一些。
- CPython 使用 GIL 造成 multi-thread 比 single-thread 還慢,要想用多 CPU 加速,可以用 2.6 內建的 multiprocessing。multiprocessing 用法不難,可是有一堆小細節會爆炸,要仔細讀官方文件。
雖說調速度很辛苦,整體來看還是值得的,而且 Python 和 C/C++ 之間溝通很容易,針對效能改寫的成本不高,更何況有測試碼撐著,修改很容易。讓我覺得用 Python 快速開發原型,針對瓶頸用 C/C++ 改寫或平行化的流程還挺不錯的。
除了以上這堆拉里拉雜的收獲,更大的收獲是學到了一個專案的演化過程。過去自己太心急,什麼都想一開始做到最好,擔心後期會愈來愈難改,災難一發不可收拾。結果是搞得自己很累,開發效率很差、程式品質漸漸變糟也不知道從何改善。後來放開心胸,延伸 TDD 的精神,先做最主要的部份,再慢慢修補小問題:像是註解、模組文件格式、行寛該定多少 [2]、某設計會不會太慢、會不會太髒等。稍微轉換思維、改變自己的習慣,會發現過去許多困擾都不見了,似乎多明白了一點「擁抱變化」的含意了 [3]。
備註
- 接觸 TDD 的概念;用它實作近十個小專案包含 C++、Java、Python 三種語言;單一專案寫了上萬行的程式。
- 我後來習慣用 85。這樣用 24″ 螢幕配 putty 預設字型大小,用 VIM 剛好可以垂直分割成兩個視窗而不會折行。
- 話說我以前一直覺得這個詞很嘴炮啊。