古語有云:「竹外桃花三兩枝,春江水暖鴨先知。」春天不會早上起來敲你家門,跟你說他來了。冬天進入春天的過程,是一天一天變化的。等你發現春天來臨時,外頭早已開遍桃花了。
我們寫程式時,結構在變「硬」有時快有時慢,但不論快慢,都不會「通知」你,都是你有一天要改一個小小功能,卻發現竟然要做非常大幅度的修改,這時才赫然驚覺:「糟了,是世界奇觀!不小心把軟體寫成硬體了!」
糟了,是世界奇觀!圖片截自臉書:高雄好過日
那怎麼辦?
今天起的連續三天,我們要聊的是這幾年大大改變筆者本身寫程式習慣的開發方法:Test Driven Development,江湖人稱 TDD。
TDD 的發明,一般認為是 Kent Beck 在 2,000 年左右提出的,但其實 Kent Beck 自己後來也說,他不是創始者,他只是把一個已經有人在用,而他覺得很棒的開發方法系統化地整理出來,讓大家更容易使用而已。不論如何,這個方法在那之後至今 20 多年來,確實改變很多人的 coding 習慣,包含他的好友,人稱 Uncle Bob 的 Robert C. Martin。
Uncle Bob 曾不只一次在公開場合聊到他第一次見識到 Kent Beck 用 TDD 的方式,以「數秒 commit 一次」的方式,慢慢完成一項工作,而且做完的同時,程式、測試、重構,三件事情是同時完成的。並且不斷描述當時他有多震驚與興奮!總之在那之後,Uncle Bob 就成為了 TDD 的愛好者與推廣者了。
近二十年後,我也是 XD
TDD 主要是由三個動作組成的:
把一個功能,拆解成多個小小的驗收條件,不斷重複以上三個動作,直到功能完成,就形成了 TDD 的基本套路。為什麼說是「套路」?因為這只是一般形式,要真正在工作中用得上,沒那麼容易。首先,每個循環要包含哪些內容,就是門學問了。這個我們下一篇會深入討論,這裡讀者先了解原則即可。
上面所謂的原則,就是「紅綠燈原則」與「Baby Step 原則」。
圖片截自 http://www.anecon.com/blog/tdd-baby-steps/
在第一步依需求寫出一個測試後,不囉嗦,直接跑下去。這時測試會過就有鬼了,我還沒寫程式哩!所以 fail 是正常現像,請安心服用。這時 IDE 一般會以紅色標示,所以我們稱之為「紅燈狀態」。
接下來要做的事,就是寫程式讓它通過。這時,我們要秉持 Baby Step 原則,「盡量少做一點事」,或是「少做一點設計」,只求「這個測項」能通過,其他可以先不管。這時再跑測試, IDE 會呈現綠色,進入「綠燈狀態」。
此時很多人會直接進入下一個測項,而 Kent Beck 則建議我們且慢,先找看看有沒有可以重構的地方。接著跑測試,因為是「重構」,根據 Martin Fowler 的建議,重構不應該改變物件對外表現,所以這時,測試應該維持綠燈,也就是全部通過才對。
請特別注意第三步的重構,很多人使用 TDD 會掛掉,就是掛在這裡。在操作 TDD 時,因為每一步都很小,所以我們都以為沒什麼好重構的。還記得前幾篇中的「發獎學金」的例子嗎?事實其實是,在加入新需求時,儘管需求看起來不大,但程式的「擴展」與「變醜」,是一下子的事。如果我們不經常地停下來看看程式的樣貌,一看到壞味道就重構掉,等程式多完成一兩個測項再回頭來重構,很可能已經變得太醜而無力回天了。
說到底,為什麼 TDD 會被設計成這個樣子?因為它希望我們「隨時」保持程式的可運行性與整潔度,並且不要過度設計。而這些目的是怎麼達到的?筆者閱讀過一些書籍,加上自己一些經驗與心得後,整理如下:
回想第一步,先寫一個錯誤測試的原因其實有二。第一,我們想要先確保接下來要加的功能,在程式寫完後,能滿足需求,於是先用測試來描述它。一但測試通過,就能說明程式是滿足需求的。第二,我們必須確保測試此時是 fail 的,因為當我們還沒加程式,測試就通過了,這代表這個測試什麼也沒測到,屬於「無效測試」,應該立即刪除,否則會損害測試的整潔度。很多人 TDD 越做越慢,測試整潔太差也是主因之一。
第二步,我們應該做「只能再多通過這個新測試」的邏輯。為什麼?因為多做事會多花時間呀!在下一個測試還沒出生之前,任何「超前部署」都是多餘而不實際的。
很多人喜歡在一開始就花大把大把的時間想好了完整設計,但事實上經常會動手做到一半後又腦袋一拍:「啊!我想到一個更好的解法了!」這時想到的解法通常會比一開始的還好,因為你手上握有的證據變多了,你更能做出較適當的判斷。如果你一開始已經有了一個非常完整的設計,並且已經做了很多先期準備,那此時你要丟棄的內容會非常多,非常浪費。倒不如一開始不要想那麼多,大方向抓住就開工,還比較實際一點,也比較經濟。
筆者的父親多年經商,他經常告誡我們:「做出來的商品,最好是每一件都能賣出去,就能把效應拉到最大。」筆者雖然後後來沒有克紹箕裘,但父親的教誨還是有聽進去的。
第三步的重構,目的就在「及早發現,及早治療」。如同前文所述,更適當的設計總是「待會兒」才會出現。每當做完一點點事情,我們應該給自己一點點時間來做這樣的審視與思考。這個一點點時間不用太久,通常二三十秒,就能夠看出端倪。
如果此時發現更好的設計,就應該直接改掉,因為我們才剛剛寫完邏輯,對整體結構非常熟悉,而且還有測試保護,此時正是重構成更好設計的最佳時間。
如果這時不做,心想著等等把所有功能都做完再來重構,恐怕到時壞味道已經累積太多,改都改不動,無法重構,只能重寫了。如果不小心走到這一步,相信很多人就會選擇放棄,就這樣 commit 後上線了。等到一段時間需要回來改這一段程式時,就要花更多時間理解,更多時間修改,如果想重構,也就要花更多時間了。這些「更多時間」都是沒有必要的花費,其實。
如果各位讀者有一種概念,叫做「TDD 就是先寫 test 再寫 code」,那真的是有很大的誤會。筆者認為,TDD 名字中雖有一個 Test,但本質上不是 Test,而是需求。就像每個 RD 都會想要「先確認需求」一樣,TDD 希望我們做的事,是「先試著描述需求」。你必須得先能成功描述需求,讓別人看得懂,或至少讓 30 秒後的你自己看得懂,你待會才有憑據去說明你剛寫好的 code 是「對」還是「不對」。
至於為什麼非得用 Unit Test 來描述呢?為什麼不寫在紙上、Google Sheet 中,或是其他地方呢?筆者認為原因有二。其一,Unit Test的結構,為「準備、跑、對答案」,這正巧與需求描述的必須元素「前提、過程、結果」對應得上。其二,Unit Test 可以執行,而且「錯了會叫」。寫在紙上的需求錯了可不會叫!
有了能不錯地與真實場景對應,而且「錯了會叫」的需求描述,RD 就可以放心大膽地對程式做任何修改與設計,反正錯了自然有人會跟我說(而且是幾乎馬上)。因此,筆者認為,拿 Unit Test 來描述需求,不是硬規定出來的,而是隨著開發者的經驗累積,在想要不斷提高效率與正確率的動力推進下,自然而然演化出來的結果。
每次與客戶或 PM 溝通需求時你是否會想直接問「你現在情況如何?你想要怎麼進行?你覺得事情變得怎樣會更好?」如果這樣的溝通方式讓你覺得舒服,且理當如此,那我認為 TDD 的開發方式應該很適合你。
謎之聲:「能自然舒服地把事做好就好,不是 TDD 也無妨。」
ithelp2021