2009年11月8日 星期日

我學 TDD 的方式

TDD ( 測試驅動開發 ) 的動機請見《為什麼要寫 unit test?為什麼要先寫測試?》。這篇簡記一下我學習 TDD 的方式:

看完書後,大致上是從大師的 Blog 跳著看有興趣(或著說,讀起來比較有感覺)的部份。自己寫個一陣子就看個幾篇,待思考消化後再回頭試著用自己的方式寫出來。如此反覆進行。最重要的是在自己的環境裡,用自己的方式進行 TDD,感受會更明確。

TDD 的概念很簡單,看過例子後應該能馬上使用,但無法立即精通 TDD 。它是一個習慣,而培養習慣需要時間累積。養成習慣後,接著需要提昇測試和重構的技巧,才能逐步提昇效果並降低使用 TDD 的成本。如同過去學 Design Pattern 或其它寫程式的技巧一般,需要讀書和時間練習。我在前幾次使用 TDD 時有犯一些錯,使得效果打折,有感受到一些好處,但也對一些壞處感到疑惑。直到寫了
五、六個小專案後,才釐清一些疑慮,確信 TDD 是很划算的取捨,從而決定持續使用 TDD。最近用它寫超過一萬行程式,更加感受到它的威力。

備註

  1. Java 的例子可見 The Bowling Game Kata;Python 的例子可見 Dive into Python 3 介紹 unittest module 的章節介紹 refactoring 的章節。《Dive into Python 3》的例子比較不像真實的 TDD,為了教學方便,作者直接寫最終版的測試碼。若要看原汁原味的演化過程,看 Java 的例子或 Kent Beck 的書較適當。

2009年10月29日 星期四

賀!Python 寫破萬行記念!

這是我獨力寫過最大的專案了,從八月初開始寫,不知不覺寫到破萬行啊,而且其中有四千多行是測試碼,貫徹當初 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]。

備註

  1. 接觸 TDD 的概念;用它實作近十個小專案包含 C++、Java、Python 三種語言;單一專案寫了上萬行的程式。
  2. 我後來習慣用 85。這樣用 24″ 螢幕配 putty 預設字型大小,用 VIM 剛好可以垂直分割成兩個視窗而不會折行。
  3. 話說我以前一直覺得這個詞很嘴炮啊。

2009年9月22日 星期二

寫 Python 測試碼的好幫手

前陣子用 TDD 寫了個六千多行的工具,這篇記錄一下過程中惠我良多的好幫手。

unittest

內建模組。

用法見官網介紹《Dive into Python 3》的第九章 Unit Testing ( 有附帶介紹 TDD ) ,如同 Java 的 JUnit 3.x,用 unittest.TestCase 來寫 test case 很方便。

coverage

安裝方式:easy_install coverage。

Code coverage 是分析測試碼品質的方法,標示出測試碼沒有執行到那幾行程式碼。使用 Code coverage 的基本精神是:Code coverage 的數據高不表示測試碼有效,但數據低的話,測試品質必定不好。至於高低要如何界定?這得看專案的類型,比方科學計算型的程式,要求 80% 以上並不過份;而有一堆圖形介面的程式,可能連到 60% 都很難。

coverage 官網有簡單易懂的使用例子。由於 Python 的功能限制,coverage 無法作到像 Java 那麼全面。coverage 的限制以及 Code coverage 的注意事項,詳見作者的說明:“Coverage testing, the good and the bad.”

nose

安裝方式: easy_install nose。

寫 unittest 時,管理 test suit 是件很瑣碎又易犯錯的事,相信很多人會想說,能不能跑個程式,自行搜集目錄下全部的測試碼並自動執行。沒錯,大家的心聲 nose 聽到了!這裡直接用例子說明 nose 的使用方式:

  • 執行目前目錄下所有測試:
    1
    
    nosetests
  • 執行目前目錄下所有測試並附上子目錄 pkg1、pkg2 的 Code coverage 資訊:
    1
    
    nosetests --with-coverage --cover-package=pkg1,pkg2 --cover-erase
  • 不要執行 slow_test.py:
    1
    
    nosetests -e slow_test.py
  • 使用四個 CPU 平行執行測試:
    1
    
    nosetests --processes=4

–with-coverage 需要先裝 coverage;–process 得另裝 package multiprocessing ( easy_install multiprocessing ),相關說明詳見 Multiprocess: parallel testing

另外,若要讓 nose 跳過物件 A 的測試,就在程式裡寫上

1
A.__test__ = False

比方若不想測模組 mod,就在 mod.py 裡寫上

1
__test__ = False

sqlite3

內建模組。

用法見官網介紹。在測試資料庫時,個人覺得 local database 比 mock 好用,方便準備資料,測起來也比較踏實,而且使用 memory mode 可大幅減少執行時間。附帶一提,sqlite 跨 C、Python、Java 等語言,支援 SQL 92,執行速度又快,相當好用。

PyMox

安裝方式: 用 SVN 從官網 checkout 原始碼,再用 setup.py 安裝:

1
2
3
svn checkout http://pymox.googlecode.com/svn/trunk/ pymox-read-only
cd pymox-read-only/
sudo python setup.py install

用法見官網文件。PyMox 是 Google 開發的 Python 版 EasyMock,我試用過 Java 的 EasyMock 後覺得用法挺直覺的,就決定用它,如此一來學一套工具可以同時用在 Java 和 Python 上。使用 mock 的好處是簡化測試碼,更完備地測試程式,像是代換掉處理資料庫、網路連線的物件。如此一來,連測試「讀資料到一半卻斷網」的應對情形,都是輕而易舉的事。若想立即體會 mock 的功效,不妨配合 mock 用 top-down 的方式作 TDD,會發覺不同的程式開發思維。

2012-08-05 更新: 我現在改用 Mockito 了,更為簡單易用 。

2009年9月20日 星期日

Honey & Clover I, II 觀後感

Honey & Clover
又看了一次 Honey & Clover 第一部和第二部,已經明白劇情發展的我,這次還是一樣被情節給牽動著,忍不住一集集地看下去,而且更能注意細節,更能感受到作者細膩的鋪陳。

看完許久,仍然無法用文筆表達情緒的我,只好照老樣子看著別人的心得,和著劇中插曲讓感覺漸漸褪去。最後的結局,究竟小育是抱著什麼樣的心情,將那份點心交給竹本的呢?

小育想傳達的心意,竹本確實地收到了。一再地重覆看著這段,我不知道自己感受到的是什麼,劇中插曲的歌詞是這麼寫著的:

平靜清澈的溪水,毫不躊躇地向前流淌。
令人懷念的午後熏風,吹拂著濕透的脖頸。
野兔不停奔跑的樣子,白百合的耀眼花姿,
夜空中閃耀的星群,都理所當然地在我眼中。

相信一定會觸碰到的幻影,
充斥著言語的底片中的街道延續著。
再見吧,再見吧,向著不知何時的你。
如果還能遇見你就好了。

只能不斷地聽著插曲,試著沉浸在兩人的心情中,隨手寫出片段的思緒。

小育和竹本彼此的心意,不是友情、愛情之類的字眼可以表示的,那究竟是什麼呢?對於他們而言,這樣的分別又代表什麼呢?為什麼如此的不捨?為什麼如此地感傷?滿腹疑問的我似乎明白答案為何,卻無法述諸言語,或是不願述諸言語。於是不斷重覆聽著插曲、重覆想著兩人的心情、隨手敲著鍵盤,讓這份情感深刻地注入內心深處。對於祝福的人以及被祝福的人都能幸福,感到無比地欣慰,只是感傷也隨著揮之不去。

2009年9月6日 星期日

康特的難題

書籍基本資料:《康特的難題》

看到中後半時,由於相當在意某位角色出奇的應對方式,讓我不得不硬衝了兩小時看到結尾,導致我破例到淩晨兩點才睡,上回這樣挑燈夜戰讀小說,應該是國高中時看金庸小說吧?

若以一般小說來說,算是普普吧。但這本揭開科學界的生活,並用非科學界人士質疑科學界視為常識的文化,還有點出女性科學界的困境,都很有意思,可滿足我對科學界人性的好奇心。像是學界慣於使用的「我們」,究竟意指為何?作者順序的含意?提出想法和執行實驗何者較為重要?實驗室成員是合作者,還是手下?害怕被盜用想法而不敢發表,以及急於發表以追求第一發現者榮譽的衝突?實驗數據不如預期,做些「適當」的調整,是修正誤差還是照假?女性學者在追求終身職和小孩之間的抉擇?書中點出許多與科學誠實、無私、公正、理性、分享等特質矛盾的人性弱點,有許多令人深省的地方。角色的佈局也滿有意思的,將幾位看似無關的核心角色,慢慢地交錯在一起,若起頭和結尾能更圓潤就更棒了。

看完這本書後,讓我更不想走學界路線,我明白自己很難抗拒種種誘惑堅持自己。即使能堅持自己,也很難要求合作者照自己的要求進行,有太多灰色地帶,很難令合作者與自己達成共識。還是遠離利益得失,才能自在地以自己的步調前進。

2009年9月5日 星期六

驗證第一、想法第二

看到這篇《The Only Truly Failed Project》,其中有兩段話寫得很棒:

Failure is a wonderful teacher. But there’s no need to seek out failure. It will find you.

The only truly failed project is the one where you didn’t learn anything along the way.

最近觀察不少數據和實驗結果,推翻之前許多猜想,不過我沒有像碩士做研究時那般痛苦,反而有些興奮。大概是因為沒有發論文的壓力吧,加上先前讀過費曼的言論,覺得這是理所當然的情況,我們知道得本來就很少,想法通常也是錯的。然而,重點在於如何從錯誤的嘗試中學習?

昨天晚上不斷思索這堆錯誤的嘗試告訴我什麼,仍未有定論。忽然想到愛迪生的話,當他實驗千種材料失敗後,他說,至少我明白這千種材料不能作為燈炮的材料。
於是心裡又更踏實了。但這不表示可以隨意的嘗試,在沒有有效的驗證方式之前,無謂的嘗試無法提供任何訊息。

印象中費曼提過,他不和人討論無法驗證的想法,那是沒有意義的行為。提出猜想後,我會思考如何驗證。直到有辦法進行驗證前,我會先保留想法,改試別的作法。或是將原問題先拆成幾個小問題,讓小問題能夠被驗證。待各個擊破搜集到一些資訊後,再回頭看是否能驗證原本的想法。

能夠驗證的想法,才能從中學習。不然結果出來後,我們無從判斷結果有多接近目標,自然也無法從中學習,進而做修正。舉例來說,如果要做 AI 下棋的搜尋,要怎麼知道目前的策略有效?和人下棋是個辦法,可是沒有無法頻覆進行,也就不能確保方法的可靠程度。另一方面,光看評估盤面函式的輸出值也無用,沒法確認分數高確實是有利的盤面。

換個想法,提兩個策略,互相對戰。至少可以確定每次都贏的策略,是相對來說較好的策略。一個策略可以用 greedy algorithm,另一個用自己想的特別方式。雖然我們預期特別的方法會贏,但若結果相反,也能從中明白新東西:像是 greedy algorithm 比想像中的有效,可從中找到好點子;或是特別的方法不如預期地有效,也就不用再和人下棋,減少後續測試的成本。若擔心兩個方法自動對戰變化有限,可以引用不同棋譜的中盤局面,再引入兩個方法擔任不同角色,觀察好的方法是否在各種相同局面裡,無論擔任那一方,都能下得較好。

再舉解魔術方塊的例子。為了能夠驗證,可以先將解好的盤面一步步弄亂,記下正確的還原步驟和所需步數。準備好多組測試盤面(即最後弄亂的樣子),測試演算法解的效果,步數和預期步數相差多少?那一步發展變得不同?於是有確實的數據可以分析,明白問題出在那。有未知的大進步時(只花了預期的一半步數),也方便觀察出原因(如弄亂盤面時做了不當的重覆操作),不會不小心高估方法的成效。

看起來理所當然的事,沒想到我這麼遲才明白,驗證是如此的重要。而這個觀點卻是從 coding 那邊先萌芽,才接著在研究這邊確實實行。不論是研究還是coding,我認為重要性是: 需求 (動機) > 驗證 > 想法 (解法) > 實作。如《管理是什麼》書裡提到,管理即為客戶創造價值 -- 相當含糊的定義,可是卻非常精闢。不論是研究還是寫程式,也要先確保能滿足某種需求,之後的發展才有意義。接著,在天馬行空地想解法前,先確保有方法驗證方法的好壞、達到目標的程度、有辦法分析結果,之後才能確實地落實想法。於是,即使失敗仍能有所收獲。

2009-09-06 Update

LCamel 一提,發現英文的用詞很有趣,Verification and Validation 裡這麼解釋:

It is sometimes said that validation can be expressed by the query “Are you building the right thing?” and verification by “Are you building the thing right?” “Building the right thing” refers back to the user’s needs, while “building it right” checks that the specifications be correctly implemented by the system.

意即:

  • 滿足需求 = validation = do the right thing.
  • 驗證作法 = verification = do the thing right.

語言真是奇妙啊,用一句話來總結,就是「Are the do you right thing right?」

2009年8月10日 星期一

《別鬧了,費曼先生》閱讀中的雜記

讀《別鬧了,費曼先生》的過程裡,偶而有寫片段感想。這篇是這些短記整理後的記錄。

通用的學習法則

《別鬧了,費曼先生》提到費曼常跨領域學些不同東西,不管是哲學、數學、生物學,費曼都用他學習科學的習慣判別對錯,掌握基本原理。最近做研究也有這種體會。即使沒有背景知識,有良好邏輯,掌握假設和觀察結果,通常能做出正確的判斷。前提是我們能明確判讀那些是客觀事實,那些是主觀判斷,數據不會騙人,可是人會,而且我們自己也可能會騙自己。

舉例來說,看到別人的實驗結果和實驗報告,一定要自己讀一遍數據,不然無法判斷別人說明中那些部份為主觀推測。像是兩個方法有效程度差了千分之一,是否可說有顯著差異?去除人有私心的因素外,我們也沒有想像中的聰明,容易將一些相似的資訊混在一起,不知不覺認定是事實。「誤把有相關性視為有因果關係」是常見的例子,有興趣的人可以看些寫給大眾看的統計書籍。反用「判讀個人觀點和事實」的原則在自己身上也很有效,可以找到自己思路的盲點,不會過於武斷主張自己的看法。

此外,舉例說明是最佳的討論方式。費曼提到他和數學家討論數學原理時,他的判斷方式是心中想像一些實例,對方描述問題時,他就想像心裡那些物體會怎麼變化,於是當對方說某某性質如何,他發覺在他想像世界裡有矛盾時,就能做出辯駁。另一方面,我相信只有當我們能舉例說明時,我們才算真的明白,藉由逼自己舉例,可以思考的更靈活,找出忽略的細節。

別死記名詞

另一方面,費曼也指出許多人記了一堆名詞,卻沒掌握基本精神,他在 MIT 的大學同學如此,愛因斯坦的研究助理也會犯這種錯。費曼一再提到很多學生只會背名詞,不會理解背後的含意,不明白他們在學什麼。這種精神從他小時候就存在,和父親在林中散步時,費曼的父親向他解釋生物行為而不背名字,費曼將自己的成就歸功於父親童年時的教導,不無道理。

費曼在問學生問題時,會舉例討論,而不問教科書式的問題。我在確認別人能力時也是這麼做,不問專有名詞解釋(像是請說明 X 演算法為何),盡量用應用題直接看對方怎麼運用知識,這樣能立即明白對方有沒有理解背後含意。反過來看,不知道某些名詞不代表什麼,就算對方不知道 Dijkstra’s Algorithm,但若能提出相似的解法,反而更高明。只要和對方說明這些知識,他們立即能掌握精神加以運用。反之,知道 Dijkstra’s Algorithm 卻無法活用,知道更多演算法、累積更多經驗,成長也有限。

名詞只是方便溝通的工具,若不能理解背後的含意,光記許多名詞毫無意義。在資訊爆炸的時代,每天產生一堆新的名詞,吸收這堆雜亂的詞彙時,得時時提醒自己,是否明白背後含意。以前我曾落入背誦知識為樂的陷阱,後來發現知道很多皮毛卻無法做進一步運用,才驚覺自己浪費不少時間。舉例來說,現在當紅的 Android,即使我知道 Android 是 Google 發佈的手機平台,也沒任何幫助,只是一堆名詞堆砌。到不如了解為何會有 Android?適合解什麼樣的問題?需要時可視需求對相關知識做進一步研究。 (備註:我並不了解這兩個問題,只是舉例而已。)

並不是說記名詞毫無意義,費曼童年時自己發明一套數學符號,他覺得讀起來比較好理解,很高興地用了一陣子。但後來發現他無法和人溝通,就把這些符號丟了,改用共通符號。與人溝通需要專有名詞,不然難以進入問題核心,講半天還在說「你是說如果有 n 個點,點可以是任意東西,像是城市,或想像成座標也行,這 n 個點之中部份兩點有互相連接,連接的距離長短不一,而你想從某個起點 A 走到終點 B,想問如何走才能花最少的步數嗎?」(即最短路徑問題)。

2009年8月5日 星期三

《別鬧了,費曼先生》讀後感 -- 關於科學的品德

小時候曾讀過一次,當時只覺得費曼這人真有趣。走過一遭研究的生活後,再回來讀這本書,幾乎每部份都令我深思許久。特別是最後一篇《草包族科學》讓我有更深的體會。

這篇是費曼於1974年在加州理工學院的畢業典禮演講,有別於其它故事,單刀直入說明科學研究最重要的課題--科學的品德,也就是對自己誠實,對大眾誠實。費曼舉了個反例說明何謂不誠實:

有個朋友在上電台節目之前跟我聊起來,他是研究宇宙學及天文學的,而他很感困惑,不知該如何談論這些工作的應用。我說:「根本就沒有應用可言。」,他回答說:「沒錯,但如果這麼說,我們這類研究工作就更不受支持了。」我覺得很意外,我想那是一種不誠實。如果你以科學家的姿態出現,那麼你應該向所有非科學家的大眾說明你的工作--如果他們不願意支持你的研究,那是他們的決定。

自己做研究也是一樣,該同時說明支持和不支持的數據和觀點,而非只報告支持的部份。要延伸別人的研究前,要先重製對方的實驗,如此才知道兩者在自己的實驗環境下差多少,而不能為了急著產生報告,省略前人的實驗,直接做自己的部份。

做研究必須客觀,早就同意某種觀點而開始做實驗,和從實驗中找出支持自己觀點的證據,是截然不同的行為。在利益衝突下,我們很容易欺騙自己,得非常非常細心且耐心地排除任何有利於自己發展的想法。很多時候我們難以把持,或是不自覺地騙了自己。舉例來說,看到違反預期 (不利) 的結果,會不斷思考那個環節有誤,嘗試修正「不當變因」;看到符合預期 (有利) 的結果卻會很興奮,忘了要進一步確保沒有「不當變因」影響結果。

藉由費曼生活中的插曲,看到一位真正科學家自然行事的風格,令我受益良多。在看到這麼多令人困惑的情況後,費曼的身教無疑是個強心劑。我想我不會再困惑了,若有那麼一天我打算讀博士,除了找到非作不可的研究外,還要確定我能把持住想法,能完全地誠實才會成行。不然,我還是遠離利害關係,至少在沒有利害關係的影響下,我能較誠實地進行研究。

費曼演講的最後一段如此說道:

因此我只有一個希望:你們能夠找到一個地方,在那裡自由自在的堅持我提到過的品德;而且不會由於要維持你在組織裡的地位,或是迫於經濟壓力,而喪失你的品德。

我誠心祝福,你們能獲得這樣的自由。

So I have just one wish for you—the good luck to be somewhere where you are free to maintain the kind of integrity I have described, and where you do not feel forced by a need to maintain your position in the organization, or financial support, or so on, to lose your integrity. May you have that freedom.

看到這段話,我鼻頭都酸了,祝福自己,也祝福所有對科學有興趣的人。

ps. 英文原文講稿在此

2009年7月25日 星期六

學而不思則罔,思而不學則殆

現在回顧兩年前思考未來出路的文章,覺得孔子說的「學而不思則罔,思而不學則殆。」真是太有道理了。再多規劃,聽再多情報,若沒有親身體驗,只會對紛亂的情報感到困惑。

現在回頭比對當時搜集的情報和現在知道的情報,發現有許多出入,甚至連對自己的認知都有誤。一開始我不知道自己喜歡做研究還是做產品。結果經過四個月全心投入研究後,覺得做研究滿有趣的,和正在業界的學長聊天,覺得自己似乎沒那麼適合當工程師。結果去一家公司實習四個月後,又覺得自己比較喜歡做產品。現在做了半年半研究半產品的工作,又覺得好像都很有趣。到頭來是看目標是否明確有意義、組織是否能給我彈性的空間發展,以及不需要為了衝積效而做無意義的事。那麼,不論是研究還是產品,都能從中找到樂趣。若用負面例子來看,寫不踏實的學術報告,和做難用的產品,都一樣乏味。

需求不明確,情報又不見得正確,怎麼能做出有效的決定?

所以,若碩士畢業生沒有先到業界實習過,不清楚自己的喜好,聽再多情報也難以判斷。另外,若不是和公司內的人直接一對一聊,情報價值也難以判斷。和一家公司內的一個人聊,也只能了解該組詳細情況和公司概況,容易以偏概全。就像好學校有壞實驗室,壞學校會有好實驗室一樣,和自身最相近的案例才有幫助。

類似的例子俯拾皆是,像是高中生選大學,若自身沒有針對有興趣的科系先嘗試一下,和他說明學校科系的特色,也難以定奪。若用系統來解釋,沒有寫過系統程式直接讀作業系統的書,只會覺得抽象得令人不知所云。寫一堆系統程式不看書消化整理知識,需求稍微變更,又不知該如何有效處理。

學技術時我明白孔子這兩句話的意思,但在人生方向的課題裡,我卻到現在才明白:原來我沒搞懂。話說回來,接觸新領域時,總會想省時間偷吃步,不做基礎練習,看來在技術方面,我也沒明白這兩句話的重要性。

2009年7月22日 星期三

學程式語言的樂趣

國三時為了做北市數學科展,實習老師教我使用 Microsoft Word,這是我首次體會到電腦在遊戲之外的樂趣。為了學會怎麼灌 Windows,高一時參加了社團,結果社團學長 Scott 說:「灌 Windows 太簡單了,你來學 C 吧。不過我們這裡只有 Red Hat Linux,就邊學 C 邊學 Linux 吧。」傻傻的我,以為 Linux 裡只有 Vi,就四處翻 Linux 入門書,看完各本書裡 Vi 部份,還寫了篇 Vi 的教學。日漸熟悉 Vi、bash 指令後,覺得玩系統、寫程式真有趣。於是上課無聊就偷看電腦書,下課耍自閉繼續看書,中午放學都跑去社團混,就這樣渡過我的高一。不知不覺,我打下日後學習能力最重要的基礎。

高一一時想不開,同時學 C 和 Perl,用 Perl 寫東西頗有趣的,特別是費盡心思把程式擠到一行作完一件煩人的小工作時,有說不出來的成就感。可是我基礎太弱,無法明白兩者更深層的差異,只能當作兩個不同工具看。兩者同時學的下場,學一陣子仍不熟兩邊的語法。於是我先專心學 C,之後 Perl 也忘光了。那段日子最大的收獲是,不要一次同時學兩個語言。另外,多虧 Scott 心理不正常,用 K&R 的《C 程式語言》當入門書,我直接學會標準語法,和 C 的典型寫法。這個習慣一直伴隨我到現在,每學一個新語言時,我傾向直接找能教我典型寫法的書,省去先花時間學一堆語法,再去蕪存菁地寫出典型風格。

寫了三年的 C,大一接觸到 Java 後,覺得 OOP 很有趣,這麼簡單又優雅地,讓我能同時使用兩個 Stack!雖然一開始寫著滿滿的 C-like Java,程式裡都是 static method,看了些 OOP 的書,開始轉型寫著自以為是 OOP 的怪東西。時至今日,我仍然不明白要怎麼寫出真正的 OOP。至少我發現自己寫得不是 OOP,和大學時相比,算是進步不少。

有些課要求用 C++,我就會用 Java 學到的 OOP 觀念和 C 的經驗來寫。每次要寫 C++ 就重翻一下 C++ 快速入門的書,寫完後又忘光一切。大三做專題時,我和朋友們打算參加一個國外的比賽,由於主要贊助商是 Microsoft,我們決定用 .NET Framework 開發。就這樣,我有個好機會寫 C#。亂翻一些 ASP.NET 的書把殼刻一刻後,我找了《C# Essentials》來看。這本書如其名,薄又都是重點,很快地明白 C# 的特色,讓我覺得 C# 挺漂亮的。不過寫完專題後沒繼續寫 C#,現在什麼也不記得了。而且,當時我的底不夠深,仍無法感受到各語言的特色。

除 C++、C# 兩個插曲外,大學時主要用 Java 和 PHP。用途很簡單,接網站案子用 PHP,其它用 Java。我練習 PHP 的方式是不斷改寫自己的網站,還有接些小案子,可是我覺得 PHP 實在不怎麼美,沒動力學好它,能用就好。

大學中途有試著寫了一點 Python、Scheme,可惜無法持續。一直到碩一一時興起,一方面是厭煩了用 PHP 寫重覆的東西,另一方面想試看看當紅的 Ruby on Rails,就邊查邊寫完成一個小案子。寫得過程裡,覺得 Ruby 很奇妙,於是在寒假花了一週讀完《Programming Ruby》第一部份,這才正式開始我學的第三個語言。

自那之後,我只有寫 Ruby,Java、PHP和其它語言碰也沒碰,這樣直到碩士畢業。沒錯,我的碩士論文也是用 Ruby 寫的,這才深深體會到 Ruby 有多慢有多肥,幸好實驗室的機器很壯,不然我得改用 Java 完成碩論。這段期間,把一個論文裡用到的程式零零總總地加起來,大概有兩千多行,也驚覺 Ruby 幫我省了不少寫程式的時間,相信用 Java 的話,我無法在不到一個月的時間裡寫完自己的演算法、數個比較對象的演算法、前置處理、評估分數等程式。不過卻辛苦交接的學妹,這樣瘋狂用一個不熟語言趕出來的程式碼,原以為不會繼續用的說。

用 Ruby 讓我有了第二次衝擊,就像當年從 C 到 Java 發現可以同時有兩個 Stack 那般驚奇,沒想到 iterator、code block 這麼好用,腦裡想什麼,手就可以直接敲出來,不用寫一寫還要把游標移來移去。而且,Ruby 可以寫出很短又好讀的程式,我從來沒想過程式可以寫到這麼精簡,而寫到這麼精簡後,又更容易理解。

碩士畢業去一家公司實習時,該公司不用 Ruby,我藉機來試看看 Python。於是,對照先前一年多寫 Ruby 的經驗,我明白很多事。有時選擇某個語言,沒有什麼明確的理性依據,單純是個人偏好。高中時我一直想知道 Python 和 Perl 的差異,到底學那個比較划算,如今又多了一個 Ruby 列入抗爭。現在來看,即使我可以找到三者在語言設計、執行速度、社群、函式庫、上手度等各方面的比較,到頭來,選擇何者反而像是信仰。

附帶一提,若要推廣一個程式語言,良好的互動直譯器、快速隨手查的文件、精簡易讀的入門書,三者缺一不可。 Ruby 大概就互動直譯器弱了一些,而 Python 三者都完美無缺,非常容易上手。至於 Java,即使到現在,我仍不知道要推薦別人看什麼書入門。最後附上我對各語言入門方式的看法,入門書書單是針對已學過一種語言的人:

語言 互動式直譯器 隨手查的文件 入門書
C man page 《C 程式語言》
Ruby 設定後的 irb ri 《Programming Ruby》
Python ipython ipython 《Python Essential Reference》
Java CHM格式的 Java doc

( 待續 )

2009年6月13日 星期六

為什麼要寫 unit test?為什麼要先寫測試?

這篇說明寫 unit test的好處,以及為何先寫 unit test 再實作,比實作完再補 unit test 更好。這裡先列出摘要:

  • 測試碼可以簡省手動測試的時間,但有錯時無法告訴我們錯誤的源頭在那。
  • unit test 可以告訴我們錯誤的源頭在那,可是 unit test 有時間成本和維護成本。寫過多 unit test 反而有害。
  • TDD 藉由先寫測試避開 unit test 的成本問題,並帶來其它好處。

為什麼要寫測試碼?

吃燒餅那有不掉芝麻,寫程式自然要驗證程式是否正確。想像平時我們如何驗證程式?大概就是寫一寫,執行看看,看看輸出符不符合預期,換些不同輸入,再看看結果,有錯再改,改完再重跑...。測試碼可以協助我們自動化驗證,省時省力。

也許有人會懷疑寫測試碼不划算,功能常改變,到時測試碼又要重改。這個問題視情況有不同種解法,比方用較「便宜」的方式寫測試,像是準備好輸出入檔,用 shell script 執行,配合 diff 看輸出是否一致。或是用 scripting language (如 Python) 寫簡單測試。而更根本的解法,個人認為是配合 TDD,待後面說明。

手動執行測試,執行個三十年,每次花的時間還是一樣,也許還會因為手酸變慢。但寫測試碼則相反,我們會愈寫愈快,愈寫功能愈好。

為什麼要寫 unit test (單元測試)?

首先介紹不同級別的測試碼,由小到大依序為:

  • unit test:測試對象為單一函式。
  • integration test:測試對象為數個單元函式的複合體。我沒特別區分這個和 unit test [*1],以下兩者一起用 unit test 表示。
  • system test:測試整個系統。
  • acceptance test:客戶驗收用的測試。將使用情節或使用需求轉成 acceptance test,藉以確認產品滿足客戶要求,方便起見,以下以 system test 統稱。

備註 *1:在談論 mock 時,unit test 和 integration test 的差異較明顯,本文不談這個議題,總得讓大家有意願寫測試,再來談如何寫測試才有意義,你說對吧。

以測試的觀點來看,system test 的 test case 夠多,system test 應該能涵蓋到大範圍的產品碼,如此一來也達成我們一開始提的「驗證」目的。從實務觀點來看,寫愈多測試碼,負擔愈多,規格變動時要改的東西也多。更何況 unit test 著眼點更細,變更實作方式也有可能得改 unit test。如此一來,為什麼要寫 unit test

首先,unit test 著眼點比 system test 小,也意味著當測試結果不對時,unit test 可以指出更明確的問題點,而 system test 只能粗略地和我們說輸出不對,接著我們得比對正確和錯誤的輸出,開始推測 bug 在那,並用 debugger 設中斷,或在程式內輸出訊息一步步找出問題源頭。相信大家都能明白除錯的痛苦。而 unit test 可以協助我們釐清那些程式是對的,那些是錯的,將問題範圍縮小。若 unit test 寫得好,幾乎用不到 debugger 和輸出訊息,光看那個 unit test 錯誤,就知道 bug 在那。

除了幫自己省時間外,unit test 也可幫助別人維護和理解程式。維護的人不如自己熟悉,難免不小心改爛程式,unit test 可以減少這類問題,也讓原作者安心給其他人修改。新手要閱讀程式時,unit test 可看成是各函式的使用範例,相信大家都同意讀例子比讀全部程式碼來得容易吧。好的範例有時比註解還有用。

為什麼要先寫測試?

好吧,若讀者大人耐著性子看到這裡,想必已對測試碼已有些心動,也認為寫 unit test 有那麼幾分道理,那先寫測試和後寫測試有什麼差別?若後來寫的程式不管用,需求變更,先寫測試不是搬磚砸自己的腳,自找麻煩嗎?事實上正好相反。

我們得先問自己,為什麼程式會不管用?為什麼需求會變更?去除客戶或主管找麻煩的因素外,一部份原因為思慮不週,寫的功能不夠貼近目標。如同產品一般,沒試用過我們不會明白產品的缺點在那,函式也是一樣。先寫測試碼就是先想像要如何使用即將要寫的函式,在寫測試碼的同時,我們同時也在設計函式。有時會發現難以測試,而想出更簡潔的介面。當我們能輕鬆寫出測試碼時,也意味著目標函式易於使用,之後才方便重用。

再者,先寫測試,相當於先列需求,規範中的輸入為何?預期輸出為何?可能有什麼例外情況?於是,我們接著寫出的程式,一定是有用的。我們不會忽然想說「要不要加個 foo(),之後大概會用到吧。」先寫測試,確保之後寫出的每一行產品碼都是有意義的。同時,我們寫的測試碼也是有意義的,若事後再補測試,不明白加入一個 unit test 有何幫助,有時會多寫不必要的 unit test,如同前面提過,測試碼不是萬靈丹,它也同時是負擔,需求變更時,有時也得一併改測試碼。再者,先寫測試表示有先考慮程式的可測性 (testability),使得程式容易測試,通常會將程式拆得很細,待寫產品碼時容易將寫好的小函式組成目標功能;完成產品碼後再來補測試則困難許多,由於寫產品碼時沒有考慮到寫測試的需求,結果是影響測試碼的品質,甚至變成過於難寫而不補上測試。

除幫助設計、減少寫冗碼的機會外,先寫測試還方便我們之後進行重構。沒有測試碼的重構是很危險的,而沒重構的程式碼也是很危險的 [*2],就像在底層不穩的地基上不斷加蓋偷工減料的高樓一般。先寫測試的好處還有一點,早寫早享受,先寫測試,就能先自動化驗證,省去所有手動驗證。

備註 *2:為什麼要重構?這是一個值得獨立討論的議題,主因為減少隱藏的 bug 和強化程式碼的可讀性,並更容易重用,以及增加新功能。詳見 Martin Fowler 寫的 《重構:改善既有程式的設計》

綜合以上所言,先寫測試花費的時間,可以攤算在設計時間,以及減少寫無用程式、減少手動驗證、縮短除錯的時間,所以先寫測試不見得會增加額外時間,我們只是把投入不划算事上的時間,先拿來寫測試。

特別加贈:何時該寫新的測試碼?

雖然寫測試好處多多,也別卯起勁來狂寫,最後卻覺得投資沒回本,浪費過多時間寫測試。遵照 TDD 的原則,先依規格寫測試,別浪費時間寫也許有用的測試。

那麼,何時才是跳出 TDD 迴圈,另外加寫測試的時機呢?依我個人經驗,我發現以下三個情況,都是寫新測試碼的好時機:

  • 在程式裡輸出訊息找錯誤:與其花時間寫 print、看輸出訊息、再回頭砍掉 print,不如寫個 unit test,之後能持續受惠。
  • 執行 debugger:同上,你還是得花時間設中斷,一步步看訊息,不如寫個 unit test,之後能持續受惠。
  • 不知錯誤在那,該如何進行下一步:這表示一次貪心實作太多功能,把目標縮小,一步步補 unit test 吧。

另外,在解 bug 時,先寫個 unit test 重製問題,再開始除錯。如此一來可確保自己明白成因,之後同樣的 bug 也不會再出現。

超級加贈:若 TDD 這麼好用,為啥它不遍及?

施主,這個問題得問你自己啊,快來加入推廣 TDD 的行列吧!

說正經的,雖然上面將寫 unit testTDD 說得如此美好,寫 unit test 不是簡單的事。若寫得不好,就會有「改變實作,也得改變測試」的副作用,或是測試執行過久,不方便反覆執行,或是寫出無用的測試。如同學習 design pattern 一般,unit test 也有 design pattern,對沒接觸過寫測試碼的人來說,相當於要花「兩倍」時間學習。加上前面提到大家可能有的誤解,使得 TDD 難以落實。

另一個可能是,若你的組織同時有兩批人馬對同一產品進行開發和除錯,程式碼分成兩個 branch:一個加入新功能,一個除錯。重構會造成程式碼難以合併,決策者因而選擇不頻繁重構。並且,這類組織可能有龐大的人力另外進行測試 (可看成是 system / acceptance test),而誤以為不需要 unit test。

然而,長遠來看,第一個理由顯然是偷懶,只是把現在的問題延後到日後再解決。趁早把除錯的時間省下來,投資到 TDD 這積優股吧。第二個情況可能是公司考量,專業分工使得公司容易找人和培訓,我沒在這類公司工作的經驗,不知能如何對應。

詳細的 TDD 風險,可參照 What is the downside to Test Driven Development?。結論是團隊要有熱情學一堆新技術,或是有個經驗老道的開發者帶才適合用 TDD。不過,我個人的觀點是,現實世界的選擇不是非一即零,不是所有程式都能 TDD,若成本過高用 TDD 不划算,那就暫時別花時間學相關技巧,挑軟柿子吃。至少,有用 TDD 的部份就能享受到 TDD 帶來的好處。別因為一些可能想見的問題,而全盤否定使用 TDD

我對 TDD 充滿熱血,請問如何入門?

好吧,這個標題是我寫來自 high 的。若有興趣的話,可以參考 The Bowling Game Kata投影片,由設計保齡球計分程式的小例子,一步步展示 TDD 進行的過程,相信跟著例子走一次必能有所啟發,親身體驗 TDD 如何帶來新設計。若能自己先寫一遍,再來看投影片,感受會更深。或是參閱我之前寫的介紹文:《TDD 推廣:背景知識和簡介》以及《最近用 Python + TDD 心得(與 Java 做對照)》

2009年5月11日 星期一

以有涯逐無涯,殆矣。

近幾個月學習狀況不錯,學習效率好也能持久,但總覺得愈學愈多,愈學愈累,偶爾會有種茫然的挫折感。忽然想到莊子的這句:

吾生也有涯,而知也無涯。以有涯逐無涯,殆矣。

原本我曲解了這段話,還振奮了一下,想說要更認真地追求知識。忽然,我發現莊子的本意反而是點出恰到好處的生活方式,於是我的世界改變了。

體悟到這點後,我決定晚上十點後停止電腦科學相關的學習,改看看動畫、或翻翻閒書,或偶而像這樣寫寫 Blog 也不壞。待之後多存點錢,買了電鋼琴還可以來個「彈琴復長嘯」。不知為何,我覺得自己可以用更自在的方式追求更深的知識,讓生活過得趨於平衡。

忽然,又有兩年半前相似的體悟,到外邊小晃了一圈,我似乎走回正確的路了。即使不知道方向,不知道終點何在,我明白自己正踏在想走的路。

題外話,看到現在學習程式語言和機器學習的進展,不禁懷疑過去為何期望這麼久,卻遲遲沒有行動。才想起以前還有許多想做的事,像是增加電腦科學的廣度認知、寫 3D 遊戲、參加各式比賽、做研究寫論文等。許多事若沒有親身嘗試,只會念念不忘,難以打消念頭。經過前幾年的嘗試,才能像現在般沉澱心靈,專心在最有興趣的幾個點上。

2009年5月2日 星期六

空之軌跡

如果不全破的話,是不是故事就可以持續下去永不結束?玩完《空之軌跡》系列後,這樣的想法浮現在我腦裡。這是去年十一月、十二月的事了,愈是喜歡一個作品,愈難下筆。

FC

《空之軌跡》是 Falcom 公司出的系列作,分別為 《空之軌跡 FC》、《空之軌跡 SC》、《空之軌跡 The 3rd》。FC 和 SC 的劇情直接相連,當初全破 FC 後發現劇情急轉直下,嘴裡雖然碎碎唸罵著 Falcom 沒天良,還是立即買了 SC 回來玩。沒想到 SC 故事又比 FC 更龐大,算算兩部玩下來,花了我一百小時,這輩子從來沒這麼密集地玩同一作品。

SC

全破 SC 後,全身充斥著滿滿的感動,又一次地為自己無法以言語表達感動而感到無力,很想分享澎湃的情感,卻擠不出一個字來。就像做了一個很長很美好的夢一般,夢醒後又回到現實世界,接著是深沉的失落感。為了紓解這種失落感,只好上 ptt 看著大家的心得,放著《空之軌跡》的背景音樂,透過不同人從鍵盤下傳達過來的感動,暫時沉浸在《空之軌跡》的世界,直到心情恢復平靜。

只是,一聽到劇中音樂時,又會陷入短暫地感傷,懷念著約書亞、艾絲蒂爾等人。劇中主題曲的歌詞寫得很好,配合歌詞聽音樂,很自然地融入當時的情景。想說的話太多卻無法述諸文字,只好藉由音樂穿插帶出我片段的感受。

《銀の意志、金の翼》,這首當作早晨起床音樂挺不錯的,情緒會很 high。FC 尾聲出現這首時,立即提昇戰鬥氣氛至最高點。

《星の在り処》 (星之所在),FC 片尾曲。配合歌詞聽著,想到艾絲蒂爾當時的感受,眼眶情不自盡地溼了。 ( 含歌詞的版本見這裡,畫質音質較差。 )

《I Swear》,SC 片尾曲。SC 的結束,暗示著艾絲蒂爾等人的旅程暫時到一段落,今後她們也會繼續活躍著。整首歌充斥著希望,但想到故事到此完結,失落感也隨之來襲。

於是我忍不住買了 The 3rd,只為多看他們幾眼。劇情方面,The 3rd 穿插許多支線劇情,帶出各角色之後的發展,大大滿足玩完 SC 的失落感。不過主線劇情相較 FC + SC,遜色不少。在看完 The 3rd 的片尾,畫面上映出「Thank you for playing」的字樣時,我更想和制作小組道謝,謝謝他們做出這樣感人的作品。也不禁想著,那一天我才能參與這樣的大作,將我的感動傳出去呢?

最後,以 The 3rd 的片尾曲,《空を见上げて》,做為結尾吧,可惜只有音樂無畫面:

sora-no-kiseki-3rd-ed-image

再見了,《空之軌跡》,謝謝你帶給我這段美好的日子。期許著有那麼一天,我也能將你傳達給我的感動,透過我的雙手,制作出足以感動他人的作品。

2009年5月1日 星期五

Python 排序的技巧

用 Python 排序時相較於 C、Java 多了效率議題。由於 Python 內建函數 (built-in function) 是用 C 實作的,排序時盡可能用內建函數效率會快上不少,詳細的數據比較可以參見 Python Cookbook,這篇做個簡單的整理。

背景知識

本篇需要 list comprehensionmap 的基本觀念,若不清楚的話可以先了解它們再往下看。

排序 list 和 dict

先來看程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
d = { 'a': 2, 'b': 1 }
# return keys ordered by key
print sorted(d)                                      # ['a', 'b']
# return values ordered by key
print map(d.get, sorted(d))                          # [2, 1]
# return keys ordered by value
print sorted(d, key=d.get)                           # ['b', 'a']
# return values ordered by value
print sorted(d.values())                             # [1, 2]
# return (key, value) pairs order by key
print sorted(d.items())                              # [('a', 2), ('b', 1)]
# return (key, value) pairs order by value
print sorted(d.items(), key=operator.itemgetter(1))  # [('b', 1), ('a', 2)]

上面的程式列出所有排序 dict ( 在一些語言裡稱為 hash table) 的方法,這裡用到幾個技巧,分別說明一下。

return values ordered by key:Python 的 method call 其實和 function call 差不多,obj.func(x) 相當於傳回 func(obj, x),所以 d.get 一樣可以傳進 map 裡,結果是會取回 d 的 value。關於 function 和 method 的說明,可以參考《理解 python 的 method 和 function 兼谈 descriptor 》

return keys ordered by value:同上的技巧,注意參數 key 有做最佳化處理,使得 d 的每個元素只會呼叫一次 d.get ( 而不是比較次數 O(N*logN) 次 ),不用擔心取 key 的函式會被重覆執行。

return (key, value) pairs order by value:這部份的 code 和排序 list 一樣,關鍵在於用了 operator.itemgetter,它的文件說明摘錄如下:

Return a callable object that fetches the given item(s) from its operand.
After, f=itemgetter(2), the call f(r) returns r[2].

以程式碼來表示的話,類似於這麼寫:

1
2
def itemgetter(n):
    return lambda r: r[n]

只要資料轉成 list of lists 的形式,就可以用 operator.itemgetter 處理。轉成 list of lists 的時間和資料數量成正比,相對於套入用 python 寫的 comparator 來說,快上不少。module operator 裡有許多好用的函式,在套用 map、max、sort 等內建函式時,可以配合使用,一來不用自己重寫小函式,二來內建函式用 C 實作,速度快上許多。

Decorate-Sort-Undecorate (DSU)

若要比較的方式比較複雜,像是希望有 stable sort,或是比較的 key 不只一個,可以套用 DSU 的解法。這裡以 stable sort 為例 (程式引用自 Python Cookbook):

1
2
3
4
def stable_sorted_copy(alist, _indices=xrange(sys.maxint)):
    decorated = zip(alist, _indices)
    decorated.sort()
    return [ item for item, index in decorated ]

DSU 的關鍵在於先把每筆資料轉成 [key1, key2, ..., original item] 的形式,如此一來內建的 sort 就會依 key1, key2, … 的順序排序,排好後再取回 original item。同樣的,排序 object 依欄位值或 method 傳回值,都可以套用 DSU。而 stable sort 的情況比較不同,是將資料轉成 [original item, original position ],排序後再取回 original item。

附帶一提,xrange 是 generator,使用 generator 好處可以參見 PEP 289: Generator Expressions

參考資料

備註

發表這篇後,才發現去年的今天我在寫 Ruby。物換星移,忽然有淡淡的懷舊感傷。

2009年4月28日 星期二

關西行:廁所大賞

從日本玩回來都快過一年了遊記還沒完成,還是直接寫最有興趣的這篇,剩下的就隨意吧。

說到旅遊方面的事,我可能沒什麼特別自信,但去一個旅遊景點先找廁所這件事,我可是很在行呢!這回去關西特別搜集了多所精美廁所外觀,在這裡和大家分享一下。為什麼沒拍廁所內部呢?一來是內部沒差太多,都是現代化設施;二來是我不想在一個人旅遊時以妨礙風化的罪行被逮補。這次的特集裡,最可惜的是沒拍到清水舞台下面的廁所,那間可說是此行中的冠亞軍之選,但人潮太多,我在那等了十多分鐘都抓不到拍照的時機。

廢話不多說,首先是金閣寺的廁所:

所照片所示,以金閣寺的名聲來說,未免太普通了點,失敗失敗。

上面這張是龍安寺石庭旁的廁所,也是相當普通。再來這張就有點意思了:

仁和寺的廁所看來比較有點格調,為了怕我自己認不出來是廁所,故意選照得到水管的角度。以在關西的第一天來說,就仁和寺的廁所有在水準之內。希望這些水準低下的廁所外觀沒有掃了大家逛大阪的興致,比較正常的風景照和介紹請參見《關西行:抵達京都、前往洛西》

再來這張是風俗博物館內部的廁所,由於該館很小,外觀...就是一扇門,只好改拍內部意思意思,可以看到中間還放了幾個小裝飾。喜歡源氏物語的人可以去看看,詳見之前寫的《關西行:京都洛中》

再貼這類照片下去,相信大家很想關網頁了,只好提前先拿出最有冠軍相的平等院廁所!

是不是看起來相當雅緻呢?而我個人相當中意的伏見稻荷大社,相當地可惜,山中的還算上得了檯面,可是在人潮多的神社旁邊,卻是相當現代化的外觀,真是太不專業了。


( 神社外的廁所 )


( 山中的廁所,拍這張花了我一點時間,雖然在山中,人潮也不算少 )

關於上兩間廁所之外的正常介紹,請見舊文《關西行:京都洛南》

下面這兩張是二条城的廁所,馬馬虎虎,將就將就啦,護城河和城牆這麼美,為什麼廁所卻沒那種霸氣呢?

而下面這張現代化的廁所,竟然位在清水寺,裝個屋瓦並不會改變現代化外觀的事實,真是太令人失望了。所幸在清水寺下面的廁所相當雅緻,為清水寺扳回一些面子。只可惜人潮過多我沒能照成,可說是本次旅遊的一大憾事。

這部份的相關介紹,請見《關西行:京都洛中二条城及洛東清水寺》

最後這張是大阪城公園的廁所,外觀看來頗為氣派,但如同水柱邊泛黃的痕跡所示,廁所內部實在太髒啦!

就這樣,本次的廁所大賞到一段落,感謝各位帶著被玷污的眼睛耐心看完。不知大家心目中關西最漂亮的廁所是那間呢?

最後的最後,我不得不說,TD-Post真是好用啊!

2009年4月25日 星期六

寫 Blog 好幫手 TD-Post!

為了方便寫 Blog,寫了個小程式讀 wiki code 產生 WordPress 吃的格式,順便藉機練習 TDD,沒想到寫了十小時才完成。剛好最近在看虎x龍的動畫,就將它命名為TD-Post (Tiger x Dragon Post) 吧。

緣起

換過多家 WordPress 用的編輯器後,我還是找不到滿意的工具。為縮短寫 Blog 的時間,我決定自己做一個。

寫 Blog 最惱人的事有幾點:

  1. 得輸入麻煩的 HTML,特別是要匹配結束標籤特別麻煩。而且 HTML code 不易閱讀,改文章時很不方便。
  2. 加超鏈結很麻煩,常用的幾個超鏈結,得重附貼多次,像 TDD 我就重貼了許多次。
  3. 無法用自己慣用的編輯器。若能用 Vim 寫 Blog,速度必能大增啊!

功能簡介

對我來說,寫 wiki code 相當方便,所以輸入格式決定用 wiki code。但 wiki 格式有許多種,我比較常用的有 PmWikiDokuWikiTWiki,其中以 PmWiki 語法最簡單,但原始碼沒後兩者好讀。最後聽從 York 的建議,採用 Tracwiki 格式。選它的主要原因除好讀好寫外,我特別喜歡它 Preformatted Text 的格式,很適合用來貼程式碼。

接著,針對第二點問題,加上自動補超鏈結的功能,只要寫過一次超鏈結,TD-Post 就會自動記下來,自動補上對應的位置。像在這段裡,因為我在第一段已寫過 TD-Post 的位置, TD-Post 的超鏈結都會自動補上。初步估計,以後寫一篇 Blog 至少可以省十分鐘,所以差不多用個六十次...這幾天的辛勞就.就回本啦。

程式下載

本程式採 BSD License ( 簡單說就是隨便使用 ),歡迎大家玩玩。雖然沒什麼註解,但我照 TDD 流程寫的,測試碼應該很完整。這裡是一些相關資訊:

  • 下載:http://code.google.com/p/td-post/downloads/list
  • 程式語言:Python
  • 程式碼總行數:682 行
  • 測式碼:346 行 ( 主要是準備測試的例子 )
  • 非測試碼:336 行
  • 實作時間:原本預估兩小時完成,卻花了約十小時。大概是一小時查語法、一到兩小時寫測試。其中花最多的時間在解決 list 和 performatted text 的衝突,這部份從開始實作到結束花了三小時才解決。

其它

有原始碼有真相,最後附上本文的原始碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
為了方便寫 Blog,寫了個小程式讀 wiki code 產生 [http://wordpress.org/ WordPress] 吃的格式,順便藉機練習 [http://en.wikipedia.org/wiki/Test-driven_development TDD],沒想到寫了十小時才完成。剛好最近在看[http://zh.wikipedia.org/wiki/虎與龍_(小說) 虎x龍]的動畫,就將它命名為[http://code.google.com/p/td-post/ TD-Post] (Tiger x Dragon Post) 吧。
 
==== 緣起 ====
換過多家 [WordPress] 用的編輯器後,我還是找不到滿意的工具。為縮短寫 Blog 的時間,我決定自己做一個。
 
寫 Blog 最惱人的事有幾點:
 1. 得輸入麻煩的 HTML,特別是要匹配結束標籤特別麻煩。而且 HTML code 不易閱讀,改文章時很不方便。
 1. 加超鏈結很麻煩,常用的幾個超鏈結,得重附貼多次,像 [TDD] 我就重貼了許多次。
 1. 無法用自己慣用的編輯器。若能用 [http://www.vim.org/ Vim] 寫 Blog,速度必能大增啊!
 
==== 功能簡介 ====
對我來說,寫 wiki code 相當方便,所以輸入格式決定用 wiki code。但 wiki 格式有許多種,我比較常用的有 [http://www.pmwiki.org/ PmWiki]、[http://www.dokuwiki.org/ DokuWiki]、[http://twiki.org/ TWiki],其中以 [PmWiki] 語法最簡單,但原始碼沒後兩者好讀。最後聽從 York 的建議,採用 [http://trac.edgewall.org/ Trac] 的 [http://trac.edgewall.org/wiki/WikiFormatting wiki 格式]。選它的主要原因除好讀好寫外,我特別喜歡它 Preformatted Text 的格式,很適合用來貼程式碼。
 
接著,針對第二點問題,加上自動補超鏈結的功能,只要寫過一次超鏈結,[TD-Post] 就會自動記下來,自動補上對應的位置。像在這段裡,因為我在第一段已寫過 [TD-Post] 的位置, [TD-Post] 的超鏈結都會自動補上。初步估計,以後寫一篇 Blog 至少可以省十分鐘,所以差不多用個六十次...這幾天的辛勞就.就回本啦。
 
==== 程式下載 ====
本程式採 [http://zh.wikipedia.org/w/index.php?title=BSD许可证&variant=zh-tw BSD License] ( 簡單說就是隨便使用 ),歡迎大家玩玩。雖然沒什麼註解,但我照 [TDD] 流程寫的,測試碼應該很完整。這裡是一些相關資訊:
 * 下載:http://code.google.com/p/td-post/downloads/list
 * 程式語言:[http://www.python.org/ Python]
 * 程式碼總行數:682 行
 * 測式碼:346 行 ( 主要是準備測試的例子 )
 * 非測試碼:336 行
 * 實作時間:原本預估兩小時完成,卻花了約十小時。大概是一小時查語法、一到兩小時寫測試。其中花最多的時間在解決 list 和 performatted text 的衝突,這部份從開始實作到結束花了三小時才解決。
 
==== 其它 ====
 
有原始碼有真相,最後附上本文的原始碼:{{{
#!html
_QUINE_
}}}
 
身為資工人,這段原始碼當然也是自動貼上的啦,不過我沒用像 [http://en.wikipedia.org/wiki/Quine_(computing) Quine] 那樣的技巧,只是單純地取代關鍵字。

身為資工人,這段原始碼當然也是自動貼上的啦,不過我沒用像 Quine 那樣的技巧,只是單純地取代關鍵字。

2009年4月22日 星期三

中翻英,線上翻譯測試 - Google Strikes Back!

( 這篇寫於 2008-10-26,不知為啥從 Blog 裡消失了,所幸 Google Reader 裡有備份。 )

剛才無聊看看 Google Analytics ,發現本站最熱門的文章還是那篇《中翻英,線上翻譯測試》,我不知該說什麼,這不是件令人高興的事啊。總之,一時興起又來測看看。

最近 Google 翻譯似乎變強了,果然有在學習啊!輸入問題如下:

我要代替月亮來懲罰你

兩年前 Google 的回報令我很失望:

I want to punish you replace Moon

沒想到現在 Google 幾乎翻對了!

I want to punish you in place of the moon

google 一下會發現有不少人提到類似的句子,附帶一提,個人心目中最完美的翻譯是:

In the name of the moon, I will punish you!

其它家翻譯結果沒什麼變,就不再貼了。

另外再測了之前亂測的中文詩詞,有些變化,不過說不上是變好變壞,都很糟就是了。看來博大精測的中文仍是機器翻譯無法跨越的高牆,也是國高中生們無法跨越的牆啦。

強化 Python 在 Vim 裡的顏色

我習慣用 putty 連 Unix server 開 screen,再用 vim 寫 Python。這篇記錄如何改善 Python 的顏色。

啟動 256 色 terminal

首先將可用的色彩數增加為 256 色,先確定 putty 為新版 ( 舊版 putty 沒支援 256 色)。接著參照《讓 Vim、Screen 支援 256 色》將 Screen 和 Vim 設好,記得先用文末提到的 Colortest 測試是否有符合 256 色,成功的話,數字 0 ~ 15 為原本的 16 色,16 ~ 231 為新的顏色 (6 x 6 x 6),232 ~ 255 為新的灰階色。可配合 Colortest 看到數字值和顏色。

這裡備忘該篇提到的步驟:

  • 在 ~/.vimrc 加入:
    1
    
    set t_Co=256
  • 在 ~/.screenrc 加入:
    1
    
    termcapinfo xterm 'Co#256:AB=\E[48;5;%dm:AF=\E[38;5;%dm'

更新 Python script 偵測的格式

更新 syntax/python.vim,讓 Vim 偵測出更多種格式,像是行末多的空白,或是空白行有含空格都會被偵測出來。做法如下:

  1. 下載最新的 python.vim 並放到 ~/.vim/syntax/ 下。
  2. 編輯 ~/.vimrc,加入
    1
    
    let python_highlight_all = 1

    以顯示所有 python.vim 有偵測的格式。參照 python.vim 開頭的註解,了解細部選項。

替換顯示的顏色

  1. 下載 Vim color scheme: Wombat256.vim,並放到 ~/.vim/colors/ 下。
  2. 在 ~/.vimrc 裡加入
    1
    
    colorscheme wombat256

如此一來就有漂亮的顏色啦,wombat256 的 screenshot 見這裡

備註

這個網站有提供許多不同的 color scheme screenshot,可惜沒附 Python 的 screenshot。

2009年4月19日 星期日

最近用 Python + TDD 心得(與 Java 做對照)

最近又用 Python 寫了些程式,剛好和之前用 Java 進行 TDD 做個對照。

若不知道 TDD 的人,可以先參考這篇,TDD 的概念是依以下三個步驟寫程式 ( 順序相當重要 ):

  1. 先寫簡單的單元測試,並執行它。
  2. 用最簡單的方法實作需要的功能,讓程式能通過測試。
  3. 重構程式,並確保重構後的程式仍能通過測試。

實作部份不需多談,這裡先分別對步驟一「寫測試碼」和步驟三「重構」討論,再分享一般性的一點心得。

寫測試碼

Python 寫起測試碼比 Java 簡單許多,可以輕易地抽換所有既有物件,如 module、function、class、method。下面是一個簡單的抽換 method 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass(object):
    def hello(self, msg):
        print msg
 
def new_hello(self, msg):
    print msg + " (by replaced method)"
 
obj = MyClass()
obj.hello('Testing rocks!')  # Testing rocks!
mystub = stub.Stub()
mystub.replace(MyClass, 'hello', new_hello)
obj.hello('Testing rocks!')  # Testing rocks! (by replaced method)
mystub.restore(MyClass, 'hello')
obj.hello('Testing rocks!')  # Testing rocks!

註解表示該行輸出結果,其中 Stub 是我寫得一個簡單 class,只有十行多,可以替換和還原物件。可以從 MyClass.hello() 的輸出看出,stub.replace() 後 MyClass.hello() 被換為 new_hello(),stub.restore() 後則換了回來。完整的程式碼請看這裡

方便抽換物件有什麼好處呢?舉例來說,若 class A 有兩個 method B 和 C,其中 B 用到 C ( B 會視 C 傳回結果改變程式流程 )。為了方便測試 B 的行為,得先控制 C 的傳回結果。若是 Python 的話,照上面的例子透過 Stub ( setattr() ) 替換 A.C 即可。但在 Java 的情況,除非將 C 當作參數傳給 B,讓測試程式有機會傳「假的 C 」給 B,不然難以控制 B 內部行為。寫新程式時還有機會改設計,對於舊的程式,修改程式是個災難。用到別人的函式庫或沒程式碼就無解了。不論 Java 有何解法 ( 如採用 injection 的方式 ),解法愈麻煩,表示大眾愈不願意做,因此降低測試碼的品質,連帶影響 TDD 效果。

Mock library 部份,Java 有 EasyMock,Python 有 Mox,兩者用法差不多。後者是前者改來的,學一套兩邊都可以用。

重構

由於 Java 的特性 ( static type ),重構工具相當成熟,我慣用 Eclipse 的重構功能,離開它就不太想寫 Java。其中我最常用的功能是改名稱,包含 variable、method 、 class、 package 等。好的名稱是好程式碼的必要條件之一,好的名稱可以省去冗長的註解、縮短理解程式的時間,同時也有助於作者釐清邏輯,減少犯錯的機會。若發現名稱很難取,也許表示該 object(method / class / package)功能沒規範好,之後容易遇到問題。寫程式難免會犯錯,寫一段時間後才發現命名不夠精確,得回頭修改。若有好的重構工具,改起來快又沒風險,可以提高重構的意願。

反觀 Python 因為 dynamic type,程式碼本身提供的訊息不足(得執行後才清楚全貌),難以透過工具重構。我找了一下相關工具後沒看到滿意的。Eclipse 的 PyDev plugin 有提供重構,但試用後結果是錯的,改 module 名稱時沒動到檔名,執行後才會發現程式碼爛了。另外試了 Rope ,可以正確改名,可是速度有點慢,操作相當不直覺 ( 之後再來試看看 ropevim )。為了長遠發展考量,或許可以試看看自己弄個簡單版的改名工具或改 Rope,工具的功能可以少,但要快、容易操作且結果正確。

結語

只有在三個步驟都確實做到時,TDD 才能發揮應有的威力。依我目前一點點的實作心得,Python 容易寫測試碼、卻不方便重構;而 Java 正好相反。現在我大多有照 TDD 的流程寫程式,即使寫個一小時的小程式,也會用 TDD 。有時寫寫覺得卡卡的,才發現忘了先寫測試碼。

另外,測試碼的範圍抓得準 ( 別測太細,也別懶得測核心) ,效果才會好。我初用 TDD + Python 時就矯枉過正,寫太多測試變成 「over-testing」,省了 over-design 的負擔,卻多花時間寫不必要的測試碼、又增加日後維護的成本 。

養成 TDD 習慣的關鍵,在於寫測試碼的功力,像是如何準備 fixture ,如何改善函式介面以利測試。為了寫測試而改變函式介面並不是本末倒置,通常這會降低函式之間的關聯性,將功能明確切開,讓每個函式的輸入輸出都很乾淨(有簡單的輸入,才方便準備 fixture)。而乾淨的輸入輸出意味著函式更容易被組合使用。

2009年3月15日 星期日

軟體開發技巧的待讀書單

經過這陣子的評估,終於列出了夢幻般 (?) 的讀書清單。我的做法是先在網上看到有人推薦,到書局翻一陣子,再回來查 Amazon 的評論,最後決定是否有必要看。附帶一提,待這書單決定後又查了一下,結果發現每本書都有得到 Jolt Award,一瞬間好像以為 Jolt Award 不值錢了 :-)。

以下依暫定的閱讀順序依序說明,除前兩本外,後三本都有中文版:

1. Test Driven Development by Examples

Amazon 評論 4 顆星,由 Kent Beck 所著。全書只有 200+ 頁,用大量的例子,一步一步說明怎麼進行 TDD,相當易讀。這裡有別人寫的詳細書評

2. xUnit Test Patterns: Refactoring Test Code

Amazon 評論 4.5 顆星,不過只有七筆評論,樣本略嫌不足。 在書店試翻的感想是:好書但不易讀,而且實在是太厚了。ThoughtWorks 專出這種書嗎?

3. Refactoring to Patterns

Amazon 評論 4 顆星,但評論兩極化落在 3 和 5,最中肯的評論為: "Good ideas, but needs refactoring",值得一讀,但不好消化。讀過 Martin Fowler 的 Refactoring 並有一段實戰經驗後的最佳書藉。本書中文版《重構-向範式前進》,且是侯捷合譯的 。

4. Head First - Head First Object-Oriented Analysis and Design

Amazon 評論 4 顆星,評論裡指出本書適合初學者,另外最多人同意的評論 (Decent Introduction to OOA&D) 指出:本書不夠簡潔,並有不少小錯,若有第二版才值得推薦。 

5. Head First - Design Pattern

Amazon 評論 4.5 顆星,且是壓倒性的一堆 5 顆星。Head First 的書以易讀出名,但通常也寫得很厚,需要花不少時間消化 。 對照過於精簡典雅的 Design Pattern Bible Book,這本書親切不少。附帶一提,在書店還有看到 Head First - Algebra,真是太有趣了!可惜是教國中代數,不是教線性代數。

依目前心得來說,有四件事要學:

  1. 寫出良好的測試程式 - book 2
  2. 寫出良好的 OOP- book 4, 5
  3. 提昇重構技巧 - book 3
  4. TDD (其實就是上述三者的綜合體) - book 1

前往軟體開發聖殿之路是很遠的,希望一個月至少能解決一本,並持續地應用到實戰中,半年後就出師啦!

備註

這裡補充我曾看過且大力推薦的書藉:

2009年3月10日 星期二

TDD 推廣:結語和參考資料

前篇,這篇說明我和組員近兩週開發過程中的心得,並附上幾篇不錯的參考資料。

結語

目前我和組員只有做到半套的 TDD,有時有照三步驟的順序進行,有時是先寫程式碼,再補測試碼,或兩者交替寫。目前組員有感受到測試程式的好處,例如對程式有信心、避免因修改而破壞既有功能。組員也漸漸感受到重構的必要性。但對於先寫測試碼這點,仍有疑慮而難以進行。至少,我們盡量遵守 TDD 的結果,在寫另一個 class 時,會先補好測試碼,保持程式碼的品質。附帶一提,配合 IDE 的功能,先寫測試碼可以輕鬆地建出產品用程式的框架。像 Eclipse 可以自動產生不存在的 class、method、field,所以我常先寫測試,再用 Eclipse 建出這些東西的殼,之後再填入內容,寫起來相當快。

開發過程也不盡是順利。藉由頻繁的 code review 組員的程式碼,還有回顧自己的程式碼,我發現許多問題。像是單一測試函式太大、測試碼和程式關聯性過高、測試時間過長,若沒有第一時間更正,後果不堪設想,也會使得組員對 TDD 有不當的負面認知。這方面的相關知識可以搜尋 TDD antipattern,或 JUnit antipattern

此外,由於組員少了前備知識,進行開發過程裡會看到許多 code smell,常常在重構一段程式前,必須先重構另一段程式,而重構另一段程式前,又得先補對應的測試程式或重構測試程式。這才體會到良好品質的程式碼得來不易,需要全組成員一同堅持和維護,大家的相關知識也需要一同逐步提昇,才能達到良好的軟體開發流程。雖然初期摸索很花時間,相信日後會有相當高的回報。

說了這麼多,其實仍抵不過自己親身嘗試,強力建議親身找個小專案,用 TDD 的三個步驟寫個千行程式,相信程式設計思維會有很大的轉換。

參考資料

我到 slideshare 找了一陣子,看到三份不錯的教學,但各有些不足,沒有一份完全符合我預期的入門文件。若有時間的話,自己再來作份投影片。

相關閱讀

TDD 推廣:相關工具

前篇,這篇說明我近兩週開發過程中選擇工具的心得。

開發環境

我採用 Eclipse,配合 Eclipse 的 refactoring tool,大幅加快重構的速度。人都有惰性,好工具讓我們更願意重構,而且降低重構過程除錯的風險。對照之前我自己寫 C++ 和 Python 的經驗,現在我更常做 rename variable/method、change method signature 之類的操作。

Unit Test

經過評估後,我決定用 JUnix 4.x。JUnit 4.x 改用 annotation 的方式表示 test method,可以去除必須繼承 TestCase 的限制。如此一來 test method 命名彈性更大,而且也可以直接在 class 裡加 test method 測 private method,解決因 private 不方便測試的困擾。不過我不確定這種作法是否正確,仍需多點經驗判斷。其它 JUnix 4.x 的好處可以參照《An early look at JUnit 4》

關於 unit test 的技巧, ThoughtWorks 有出相關的書:《xUnit Test Patterns: Refactoring Test Cod》。有機會再去書店翻看看。

Mock lib

關於 mock 的知識可以參見 Martin Fowler 的《Mocks Aren’t Stubs》,解釋得鞭闢入理,除介紹 mock 外,更進一步指出用與不用 mock,其實測試思維是截然不同的(確認物件行為或確認物件狀態),有機會的話再對此另寫心得。文中的範例程式有點過時(畢竟是舊文章),現在 mock lib 的用法更方便一些。Java 的 mock lib 有許多選擇,最後我選用 EasyMockZ。除了好用外,主要是因為 Python 的 Mox 是從 EasyMock 來的,這樣寫 Python 時可以花較少心力學 Mox。

2012-08-05 更新: 我現在改用Mockito了,更為簡單易用 。

Database unit testing

若只是用到簡單的 database 操作,可以用 EasyMock 處理,。測試前準備好 mock connection、mock statment、mock resutSet 即可。一方面是初學 mock 的緣故,另一方面也是 database 相關操作太瑣碎,我花了一整個下午才用 EasyMock 完成測試程式並寫好對應的功能。事後修改挺複雜的,比方說我程式裡補了 close(),mock statement/connection 也要記得加。感覺不是理想的作法,這方面經驗尚淺,需要多點實戰。

若要測得更深更全面一些,測試對 DBMS 本身的操作( insert / delete / update)的話,用 DBUnit 會方便許多。DBUnit 的基本精神是幫測試程式準備好已知狀態的資料庫,以及比對兩個資料庫是否相等。它的核心功能是將資料庫內容和 XML 檔案互轉。所以,先建好一個資料庫,透過 DBUnit 將整個資料庫存成一個 XML 檔,日後隨時都能用該 XML 檔還原回原樣。

對測試程式而言,執行速度是非常重要的,執行速度快才能反覆地執行多次,才方便反覆開發、重構以及整合測試。所以我改用 SQLite 做為測試用的資料庫,真正的產品再用 MySQL 之類的 DBMS。SQLite 的好處是支援 SQL92 的語法,整個資料庫只要一個檔案即可。如此一來只需要讀寫本機檔案,省去網路連線的負擔,也方便建置測試環境。

由於使用 DBUnit 前得先準備好資料庫才行,我得先從既有的 MySQL database 建出同樣 schema 的 SQLite database,才能用 DBUnit 做後續的操作。原本打算寫個程式讀出 MySQL database schema 再建出 SQLite database schema。但兩者建 table 語法差太多,無法直接套用。最後我是利用 DBUnit 產生的 XML 檔,寫程式從 XML 檔中讀出 table 和 column 名稱,再建出簡單的 SQL 指令來建出 SQLite database。如此一來,每次自動測試前我先用 DBUnit 將 SQLite database 填入備好的資料,就能快速地重覆在同樣的狀態下測試資料庫操作的程式碼。待有更多相關經驗後,再針對此議題寫篇心得。

相關閱讀

TDD 推廣:影響和個案心得

前篇,這篇談 TDD 帶來的改變,以及我自己的個案心得。

TDD 的影響

只要用 TDD 重寫以前寫過的小專案,就會發現最後的產出,和原本寫的程式不同。舉例來說,會發現少了很多 setter function,因為大多情況只要用 constructor 放值即可。更進一步,會發現少寫了許多功能,而這些功能其實是原本不需用到的。

TDD 同時也幫助設計出易用的介面,透過寫測試程式的過程,可以釐清程式的使用方式。當發現程式不好測試時,通常意味著程式設計不良。測試程式也可以當作文件使用,提供使用程式的範例程式碼。

除此之外,在 TDD 的過程中,開發的速度很順暢,很少會落入長時間的除錯裡,因為程式被強迫切成許多小單位,並且每一單位都有被測試。一但出錯馬上會發現,而且知道錯在那裡。不像以往得用 debugger 追個老半天,最後發現 bug 在很遠的一小段程式碼裡,有時還是簡單的打錯字。在除錯的過程,藉由自動化測試的幫助,修改和執行可以快速地反覆交替進行,減少除錯以外的精神損耗(如手動準備執行需要的資料、設置和操作 debugger )。即使寫測試程式花的時間和除錯一樣多(我相信會是較少),至少以 TDD 的方式進行,心情會比較愉快,不會卡在一個地方太久。

TDD 的個案心得

以我最近的實作案例來說,我一開始知道會用到數個算數函數,而且這些函數日後需要抽換為不同的算法,方便我比較何種組合效果最好。舉例來說,我需要實作排序演算法 Sort(),由於資料分佈特性的改變,我可能需要實作各種演算法如 quick sort、heap sort、insertion sort、radix sort 等,並讓這些演算法的輸入輸出介面一致,方便視情況抽換不同的演算法。在這個案例裡,我需要寫多個這類型的演算法,把它們整合成一個較大的程式。

若照我原本的習慣,大概就全部套 strategy pattern,於是我可能會先寫數個 interface 或是寫個 abstract class 再套多型。至少會花半天完成這些工作,並且增加一堆「未來可能會用到」的 class。但這回我忍住了,我遵從 TDD 的三個步驟,我先統統用 static method 寫,因為這是最簡單的實作。結果寫個兩天後,我發現這幾個算數函數,其實只有一個真的需要套 strategy pattern,其它用 static method 就夠了。也許那一個最複雜的函數也不需用到 strategy pattern,總之,由於目前仍無需求,我全部都保留原樣。於是我將時間花在刀口上,優先完成必要的事。並且,我的 class space 沒有被一堆「也許有用」的 interface 和 class 汙染,避免 over-design,提高整體的可讀性。

相信大家看到這都會大喊,全部都等用到才寫,日後難道不會付出更大的代價做變動?改程式碼的代價很高,所以應該多費些心力設計好才對吧?但別忘了,「改程式碼的代價很高」是一般的情況,在有充沛的測試程式做為後盾的情況下,改程式的代價其實沒那麼高。況且,程式碼隨時都有重構,程式應該是保持在容易修改的情況,改程式的代價又更低了。

至於初期的設計要精確到什麼程度才開始寫程式,仍需經驗拿捏。我目前的作法是先有個大概的整體設計,再開始用 TDD 的方式實作各個 class,並完成細部的設計。

相關閱讀

2009年3月9日 星期一

TDD 推廣:背景知識和簡介

之前我曾獨自一人用 TDD (Test Driven Development,中譯為測試驅動開發) 的方式,分別用 Python 和 C++ 各寫了一千多行的小程式,感覺滿好的。最近剛好有機會寫一個新專案,就趁這機會開始第三次的 TDD 練習。和前兩次不同的是,這次要和一位組員合作,以 Java 開發。看來正是測試 TDD 威能的最好時機。

由於我的組員缺乏寫測試程式的經驗,剛好可以用來評估是否能在組內推廣 TDD。雖然我之前有過兩次 TDD 開發經驗,但也只是自己邊看文件邊摸索,經驗仍嫌不足。這次進行 TDD 讓我多花了不少心力查相關資料。但是為了擺脫軟體開發後期的泥沼,現在先下點苦工是值得的。這裡記錄一下過程中的心得。

背景知識

了解 TDD 前,得先知道重構 ( Refactoring ) 和單元測試 ( Unit Testing )。個人大力推薦 Martin Fowler 寫的《重構:改善既有程式的設計 》,可以了解何謂好的程式碼、如何找出該改善的程式碼以及如何改善它。書中列出多種簡單的操作流程,相當淺顯易懂。附帶一提,Martin Fowler 的個人站有許多經典好文,值得細細咀嚼。

TDD 簡介

TDD 是個知難行易的道理。知難,是因為我們很難相信它,到不是因為 TDD 概念很複雜,只要三句話就可以交待完 TDD:

  1. 先寫簡單的單元測試,並執行它。
  2. 用最簡單的方法實作需要的功能,讓程式能通過測試。
  3. 重構程式,並確保重構後的程式仍能通過測試。

實務上,這三條規則蘊含了經年累月的經驗法則。若沒親身體驗過,再怎麼解釋它們帶來的好處,也很難令人信服。這三條規則是環環相扣的,由於有先寫測試,才能安心地重構;由於有重構,才能方便地擴充功能;由於專注於最簡單的實作,省掉 over-design 的時間,才能用更快的速度完成該作的事,並有多餘時間寫測試程式和重構。藉由測試程式,程式設計師可以提高對程式碼的信心;透過重構,程式碼易於修改和重用。注意這裡強調「最簡單」的實作,這正是 TDD 精神所在,簡單的實作易於進行,複雜的考量如程式碼是否能重用、是否易於擴充,都留待第三步再做。專心正是將事做得又快又好的祕訣。

天下沒有白吃的午餐,要做到如上所述的理想世界,必須學許多寫測試程式的技巧。如同平常寫程式有輔助工具、函式庫、設計模式等,測試程式亦有對應的東西要學。舉例來說,產品用的程式需要重構,測試用的程式也需要重構。關鍵在於「treating their tests like first-class citizens. 」反之若將測試程式視為可有可無的附帶程式,只會陷入惡性循環,改變產品用程式的細節得改變測試用程式,反而變成得改兩倍的程式,以及在兩份程式中除錯。結果變得更糟,並誤認為測試程式是累贅。我剛用 TDD 時就因為沒把測試程式寫成許多小巧的函式,或是沒有寫成正確的檢查方式,造成產品用的程式是對的,卻常在測試程式裡除錯,造成無謂的損耗。

相關閱讀

2009年3月8日 星期日

讓 Python 的 unittestgui 可以載入 test suite

由於 pyUnit 附的 unittestgui 怎麼試都沒成功,我改用這個 unittestgui.py:GUI test runner using wxPython。相關說明請見之前的文章。美中不足的是,它不方便載入 test suite,所以我修改了一下這份程式。

程式下載和用法

點此下載修改後的程式碼。使用的範例程式碼如下:

1
2
3
4
5
6
7
8
9
# all_test_suite.py
import unittest
import catsinbag.card_factory_test
import catsinbag.cash_test
 
def suite():
    suites = []
    suites.append(unittest.TestLoader().loadTestsFromTestCase(some_module.SomeTestCase))
    return unittest.TestSuite(suites)

執行 unittestgui.py 後,按 Open 選擇 all_test_suite.py 即可載入。

目前這個工具仍有些問題,像是原始碼改過後,得重開檔案載入後才會生效,所以無法一邊改程式碼一邊按 Run Test。還有錯誤訊息顯示得不夠清楚,而且還是顯示在 console 上,不是顯示在 GUI 上。

修改過程心得

參照 unittest官方文件,最方便載入多個 test cases 和 test suites 的方法,是採用下面這段 code:

1
2
3
suite1 = unittest.TestLoader().loadTestsFromTestCase(SomeTestCase1)
suite2 = unittest.TestLoader().loadTestsFromTestCase(SomeTestCase2)
alltests = unittest.TestSuite([suite1, suite2])

若自己用 console 執行 unittest,只要把 alltests 交給 TextTextRunner 即可,例如:

1
unittest.TextTestRunner(verbosity=2).run(alltests)

但是,一個熱愛寫 testing 程式的程式設計師,除了想要有更高品質的程式碼外,內心更渴望看到 Green Bar啊!所以非得用 GUI 版的 unittest 不可。而 unittestgui.py 可以載入 test case、test suite,卻沒辦法載入 module 內產生的 test suite。所以只好直接改 unittestgui.py 的程式碼。關鍵在於 OnMenuOpen() 如何取得 test suite。原本的程式碼如下:

1
2
3
4
5
6
7
modname, modtype = os.path.splitext(os.path.basename(self.filename))
if modtype.lower() == '.py':
    moduleToTest = imp.load_source(modname, self.filename, file(self.filename))
elif modtype.lower() in {'.pyc':0, '.pyo':0}:
    moduleToTest = imp.load_compiled(modname, self.filename, file(self.filename, 'rb'))
#print moduleToTest, dir(moduleToTest)
self.suite = unittest.defaultTestLoader.loadTestsFromModule(moduleToTest)

修改後的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
modname, modtype = os.path.splitext(os.path.basename(self.filename))
if modtype.lower() == '.py':
    moduleToTest = imp.load_source(modname, self.filename, file(self.filename))
elif modtype.lower() in {'.pyc':0, '.pyo':0}:
    moduleToTest = imp.load_compiled(modname, self.filename, file(self.filename, 'rb'))
#print moduleToTest, dir(moduleToTest)
if hasattr(moduleToTest, 'suite'):  # use the prepared suite if it exists.
    if callable(moduleToTest.suite):
        self.suite = moduleToTest.suite()
    else:
        self.suite = moduleToTest.suite
else:
    self.suite = unittest.defaultTestLoader.loadTestsFromModule(moduleToTest)

利用已讀到的 module: moduleToTest,增加判斷它是否有提供 function suite() 或是 attribute suite,若有的話,直接使用準備好的 test suite,而非從 TestCase 或 TestSuite 中產生。

最後附上執行後的精美圖片,辛苦了這麼久,就是為了看到這 Green Bar 啊!

unittestgui_with_suite

裝了 ScribeFire Blog Editor

由於 Windows Live Editor 對 WordPress 的支援不夠完整,這篇改用 Scribefire 試看看。

Scribefire 也有很炫的預覽功能,而且多了可以將網頁的標題和連結取出加入編輯內容內,挺方便的。Category 部份到是有支援一半,可點選要加入那些目錄,但沒有顯示出原本目錄的階層關係。唯一的缺點是不能加入<!–more–>。或許之後可以改用 ScribeFire 看看。

Update

ScribeFire 也加了部份討厭的 tag,像是 <br> 和 <div>,這樣日後要回頭手動編輯時,文章內容挺醜的。

裝了 Windows Live Writer

剛才不小心關掉網頁而遺失了正在寫的文章,一怒之下就裝了 Windows Live Writer。現在這篇就是用它來試寫。

初步用起來還挺方便的,不過每打一個字畫面就閃一下,感覺挺怪的。還有不能插入 WordPress 專用的一些 tag,得自己切到 Source mode 輸入(例如<!—more—>)。 Preview mode 看起來挺屌的,可以直接看到文章送出後,在自己的 Blog theme 下看起來是什麼樣子。

Update

Windows Live Writer 竟然加了多餘的 tag,像是 <p>,而且不能用<!—more—>,發文章時找不到選 category 的地方,只有看到加 tag。改來試用別套好了。

2009年3月7日 星期六

甲土豆症候群

這個可怕的病得從小黃(匿名)去一趟系計中後說起。

有天小黃買了新硬碟,卻發現自己的電腦無法使用,只好去系計中借些工具處理一下。結果當時系計中的部份人士正瘋狂地看《甲土豆系列》影片,小黃不小心中標了,遂把影片也帶回家裡推廣。

不久後小羊(匿名)也中招了,兩人還常常你一句我一句地接裡面的台詞,這時小駱(匿名)才發覺事態不妙。但小駱經不起誘惑,仍將它介紹給小豬(匿名)、小德(匿名)、小王(還是匿名)等人,結果不幸地,小王也中標了。

在此記錄此種症狀的感染過程:

  • 通常看完第一遍時,是一臉茫然不知所措。
  • 看第二遍時,開始有噗嗤一聲。
  • 看第三遍時,開始斷斷續續地笑。
  • 看第四遍時,開始莫明地狂笑,但要說為何好笑,卻怎麼也說不上來。
  • 看第五遍時,忍不住跟著念起劇中台詞,而且沒有播放時還會不停地覆誦劇中台詞,遇到其他發病者時,會自然地你一句我一句地接完全部台詞。

為證明我不是無端唬爛,以下為小王發病過程的個案記錄:

  • 這是啥鬼….XD “靠北我就說前面那一句給你…”
  • 後勁真的蠻強的…
  • 甲土豆!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  • 甲土豆!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  • 甲土豆!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

若還是不信邪的話,請直接觀看《甲土豆》一片:

另外讓小黃和小羊中毒最深則是這篇《歐巴馬》(內有髒話,不喜者勿點):

我自己是覺得《聯合國》也不錯啦(內有髒話,不喜者勿點):

僅以此篇,為這些患者留個記錄。若看完上面兩篇還無感覺的話,恭喜你,你目前對《甲土豆》免役,別再多看幾遍,快快離開《甲土豆》的世界吧。

備註

我忘了在那看到別人介紹,這系列原本的影片是教青少年正確的災難處理方式,像是「失火了怎麼逃生」,「有人在冰湖上跌倒了,要怎麼救?」,對照改版後的台詞,真是太 XD 啦,讓我想到台詞翻譯的重要性。而說到台詞翻譯的重要性,不得不提這篇《史上最爛翻譯: 港版「風之谷」完整考證》,相信自此之後,大家會對譯者們致上最高的敬意。

路漫漫其修遠兮,吾將上下而求索

忽然發現很久沒更新「格言綿句」了,來補上去年十月末的感觸。

原文寫於 2008-10-21,下文為稍微修改後的版本。

在家和老爸老媽一起看《喬家大院》,這部戲還滿好看的。當喬致庸決定做票號 (相當於現今的銀行) 時,他的得力助手孫茂才大力勸阻,而喬致庸引用《離騷》回了這段話:

亦余心之所善兮,雖九死其猶未悔。
路漫漫其修遠兮,吾將上下而求索。

真是感動到心坎裡了!雖然我還不知道自己要追求什麼,也許過陣子會重想起學生時代的熱情。

備註

離騷全文見這裡,這輩子大概一次也不會仔細讀完吧。文學氣息這種東西,小時候沒好好培養,長大後就是那幅德性啦。要學的東西只會愈來愈多,學不完的東西也只會愈來愈多。

2009年2月23日 星期一

關西行:大阪海遊館

2008/05/26 Mon.

早上 checout 離開住了三天的京都旅館,要回大阪住商務旅館啦。

一早 7:30 起床,從京都坐電車回大阪。途中沒搶到座位,得站個半小時,真累。到大阪站後不知道怎麼買一日卷,用英文和車站服員用英文求助,大車站的服務員英文很好,真方便。還有在大阪站看到大型行李鎖箱,對觀光客來說挺方便的。沒想到回台灣後發現台北車站也有,之前都沒注意到。

早上先花了日幣 2,000 參觀海遊館,號稱全世界最大的水族館,裡面值得看的東西滿多的,大概可以逛個兩小時,對照京都一些寺廟要價近千元逛不了一小時,C/P 值還算 OK 啦。


( 這入口超炫的,我在這足足看了十分鐘多。原本想拍下一些大魚的樣子,可是游太快了,完全沒照成功。 )

過了入口隧道後,沿路的景象也很壯觀,像是下圖這隻海遊館最大的魚:

照片看起來很普通,現場則是隔著一片玻璃看著三層樓高的水池,裡面一群奇形怪狀的魚游來游去。


( 長得很像香菇的水母。 )


( 超可愛的娃娃。880日幣,買不下手啊。 )


( 可愛的企鵝娃娃,真正的企鵝也很有趣,可惜都沒照到好鏡頭。 )

逛完海遊館後,到旁邊的 GREENS 吃午餐,在大阪靠它吃了三天的午餐。可能是因為 buffet 式的,店內大多為歐美來的客人。接著前往 Universe City,才剛下電車就能聽到熱鬧的音樂。愈走近入口,氣氛愈熱鬧。和現場熱鬧氣氛相對,我忽然有股強烈的孤寂感。在入口看了一陣子後,我轉身離開。這大概是在日本自由行九天裡,繼在 PAKDD 晚宴後,感受到最強的一次孤寂感。於是我修改行程,改成先去大阪城公園。

( 未完待續 )

備註

本篇文章是在 herbage 借我的 Cherry 茶軸下打完的,無奈今天沒啥 fu 寫 blog,本篇無法打完。鍵盤打起來打起來挺舒服的,卻讓我意識到椅子不舒服啊...。

2009年2月20日 星期五

Naive Bayes Classifier 的原理(單刀直入版)

這篇內容和上篇《Naive Bayes Classifier 的原理》一樣,只是改採簡明厄要的寫法。話說我以前都寫這種風格的說。

問題描述

給定由屬性 A1, A2, A3 和類別 C1, C2 組成的 training data(例如 (A1, A2, C1) 或 (A1, A3, C2))。求 A1, A2, A3 三者任意組合的情況下,應該分類到 C1 還是 C2。

Naive Bayes Classifier 定義

以 (A1, A2) 為例說明:

P(C1 | A1, A2) = P(A1, A2 | C1) * P(C1) / P(A1, A2)

比較 P(C1 | A1, A2) 和 P(C2 | A1, A2) ,若前者較高,就將 (A1, A2) 分類到 C1,反之分到 C2。

注意 P(C1 | A1, A2) 和 P(C2 | A1, A2) 分母一樣,可以忽略不算。

Naive Bayes Classifier 原理說明

相較於直接數 training data 裡各種組合和對應到類別的數量,Naive Bayes Classifier 的優勢是在 training data 不足時,仍能處理各種屬性組合。也就是說,即使 training data 內沒有出現過 (A1, A2, C1) 和 (A1, A2, C2),Naive Bayes Classifier 仍可估計出 P(C1 | A1, A2) 和 P(C2 | A1, A2) 何者機率較高。

原因是 Naive Bayes Classifier 假設所有屬性對其類別具有 conditional independence ,所以 P(C1 | A1, A2) 可拆成如下的算法:

P(A1, A2 | C1) = P(A1 | C1) * P(A2 | C1)

同理可算出 P(A1, A2 | C2) ,所以能比較 P(C1 | A1, A2) 和 P(C2 | A1, A2) 的值。注意真實情況下 conditional independence 不見得會成立。

2009年2月19日 星期四

Naive Bayes Classifier 的原理

以前讀 Bayesian probability 時一直有個疑問,為什麼要這樣繞彎來計算?這疑問在讀 Naive Bayes Classifier 時更大了。

假設輸入為屬性 A1、A2、A3 和類別 C1, C2,為什麼不從 training data 裡直接計算出 P(C1 | A1, A2, A3) ?如下所示:

P(C1 | A1, A2, A3) = P(A1, A2, A3, C1) / P(A1, A2, A3) — (1)
P(A1, A2, A3, C1) = N(A1, A2, A3, C1) / N(U)
P(A1, A2, A3) = N(A1, A2, A3) / N(U)

其中 N(.) 表示計算出現次數,U 是宇集。或是更直接一點,直接算數量:

P(C1 | A1, A2, A3) = N(A1, A2, A3, C1) / N(A1, A2, A3) — (2)

可是 Naive Bayes Classifier 卻採用下面這種繞彎的方式子計算:

P(C1 | A1, A2, A3) = P(A1, A2, A3 | C1) * P(C1) / P(A1, A2, A3) — (3)

顯然 (2) 看來直接易懂,為什麼 Naive Bayes Classifier 要用 (3) 的算法呢?畢竟等式右邊各項,也是用 N(.) / N(U) 組出來的。

今天想了許久,終於想通了!關鍵在於 training data 不見得能完整涵蓋所有可能。比方從來沒出現過 (A1, A2, C1) 或 (A1, A2, C2) ,在這種情況下,無法從 training data 裡直接用計數的方式求出 P(C1 | A1, A2) 和 P(C2 | A1, A2) 。於是疑問轉成:「為何 Naive Bayes Classifier 可以計算沒出現過的組合?」

先從定義來看:

P(C1 | A1, A2) = P(A1, A2 | C1) * P(C1) / P(A1, A2)

由於 P(C1 | A1, A2) 和 P(C2 | A1, A2) 的分母一樣,分母可以忽略不算(事實上也算不出來),所以問題簡化為計算上式分子的值。P(C1) 或 P(C2) 可從 training data 裡算估出,但在沒有 P(A1, A2) 的情況下,難道 P(A1, A2 | C1) 可以估的出來?

答案是:「在 conditional independence 的假設下可以!」

假設 A1, A2 在 C1 發生的情況下為 conditional independence,則可得到如下的結果:

P(A1, A2 | C1) = P(A1 | C1) * P(A2 | C1)

原本要求 A1, A2 在 C1 條件下同時出現的機率,成功地轉換為計算各別在 C1 條件下的機率!於是在 A1, …, Ak 對 Ci 都具有 conditional independence 的假設下,只要 training data 內可估出全部的 P(Aj | Ci),就能計算各種 Aj 組合的條件機率!

結論

相較於直接在 training data 內算次數猜類別,Naive Bayes Classifier 的特色是用極少的機率函數 [*1] 表示出所有屬性組合的機率。但是前提是 conditional independence 的假設要成立。若 Ai 和 Aj 有關聯性 ( Ai and Aj are correlate ),會算出錯誤的數據,這時可能得用 Bayesian network

更新 (2009-02-21)

經祖佑 [*2] 說明,在這補充幾點:

  • 我誤用 missing value 一詞,文中已改正為「training data 不完備」。
  • 修改結論,將 Naive Bayes Classifier 的特色寫得更明確。

備註

  1. 若有 k 個屬性和 c 個類別,只要從 training data 裡估出 k * c 個機率函數,就能估計出所有組合((2^k - 1) * c)的機率。
  2. 祖佑回覆的原文如下(從 BBS 複製過來的,排版有點亂):

    一點個人感想

    NBC假設conditional independence的優勢是
    把很大的機率表拆成許多小的機率表相乘,所以減少了機率參數
    因此減少了training data的需求量,或反過來說讓同一筆training data資料意義變大
    而文中所提的”missing value”只是這個優勢下的一個特例而已
    事實上就算完全沒有”missing value”的情況,NBC還是可能有他的優勢存在

    因為自己說要舉例,所以只好舉例
    假設要判斷一部動畫camel喜不喜歡
    觀察的attribute是有沒有蘿莉跟有沒有身材好的女角
    同樣一筆training data [(有蘿莉,沒身材好的女角) => camel喜歡]
    在沒有假設conditional independence的情況下
    就只代表(有,無)這樣的組合camel可能會喜歡
    但是在假設conditional independence的情況下
    代表的是”有蘿莉=>camel可能會喜歡”以及”沒身材好的女角=>camel可能會喜歡”
    所以說同一筆資料的information變大

    但是很明顯的這樣假設的危險就在於也許camel只是不喜歡蘿莉身材好XD
    所以(無蘿莉,無身材好的女角)可能在NBC會被誤判成camel喜歡
    這就是有denpendency的情況下,錯誤假設造成的謬誤
    順帶一提的是我覺得missing value這個詞很怪XD
    因為有另一類問題是在處理某input attribute不存在的情況
    例如training data出現(有蘿莉,”不知道”) => camel喜歡這樣的情況

    前文所說的missing value與其說是missing value
    不如說是training data不足