我們要做所有必須做的事,好讓軟體開發穩定、可預測;但我們沒有時間做任何多餘的事。開發的四項基本活動是:寫程式(coding)、測試(testing)、傾聽(listening)、設計(designing)。
決定範圍#
我們已經有了「學開車」的隱喻、四項價值、以及十來條原則。現在可以開始建構一套軟體開發紀律了。第一步是決定範圍:我們要試著規定什麼?我們要處理哪類問題、忽略哪類問題?
延伸:那個寫不出來的 Star Trek 遊戲
作者記得自己第一次學 BASIC 時,手上有幾本涵蓋程式設計基礎的練習冊,他很快就做完了。做完之後,他想挑戰比書上小練習更有難度的問題——他決定寫一個 Star Trek 遊戲,有點像他在柏克萊 Lawrence Hall of Science 玩過的那個,但更酷。
他解練習題的流程是:盯著問題看幾分鐘,把解法程式碼打進去,然後處理冒出來的問題。於是他自信滿滿地坐下來寫他的遊戲。
什麼都沒出來。 他完全不知道怎麼寫一個超過 20 行的應用程式。
於是他退開,試著先在紙上把整個程式寫完再打進電腦。寫了三行,他又卡住了。
他需要做一些超出「寫程式」之外的事——但他不知道那是什麼。
那麼,如果我們回到那個狀態,但這次帶著經驗的眼光呢?我們會怎麼做?我們知道不能只是「一路寫到完成」。我們會加上哪些活動?每項活動被重新體驗時,我們想從中得到什麼?
寫程式#
一天結束時,總得有一支程式。 因此作者提名「寫程式」為我們唯一確知不可或缺的活動——無論你是畫圖產生程式碼,還是在瀏覽器裡敲鍵盤,你都在寫程式。
我們想從程式碼裡得到什麼?最重要的是學習。
程式碼還給了我們三樣東西:
- 理解結構的機會——當你把某個東西寫成程式,你也就有機會理解這段程式碼最好的結構是什麼。程式碼裡有某些徵兆,會告訴你「你還沒理解必要的結構」。
- 清晰而簡潔的溝通——如果你有個想法然後解釋給我聽,我很容易誤解;但如果我們一起把它寫成程式碼,我就能從你寫的邏輯裡看見你想法的精確形狀——不是你腦中所見的形狀,而是它向外部世界表達出來的形狀。而這種溝通很容易變成學習:我看到你的想法,自己也生出一個;我難以向你表達,於是也訴諸程式碼;因為是相關的想法,我們用相關的程式碼。你看到那個想法,又生出另一個。
- 不可或缺的產出物——程式碼是開發絕對不能沒有的唯一產出物。作者聽過原始碼遺失但系統仍在線上運行的故事,但這種奇獸的目擊事件越來越罕見。系統要活著,就必須保有它的原始碼。
既然非有原始碼不可,我們就該讓它服務盡可能多的軟體工程目的:
- 用來溝通——表達戰術意圖、描述演算法、指出未來可能擴張與收縮的位置
- 用來表達測試——這些測試既客觀地檢驗系統運作,也在所有層級上提供一份有價值的可運作規格
測試#
英國實證主義哲學家洛克(John Locke)、柏克萊(George Berkeley)與休謨(David Hume)說:無法被度量的東西就不存在。 就程式碼而言,作者完全同意。
作者說他很擅長騙自己相信「我寫的就是我想的」,也很擅長騙自己相信「我想的就是我該想的」。所以在有測試之前,他不信任自己寫的任何東西。測試給他一個機會去思考「我想要什麼」——獨立於「它將如何被實作」;接著測試會告訴他,他是否實作了他以為自己實作的東西。
多數人想到自動化測試就想到測功能,也就是算出什麼數字。但寫測試的經驗越多,作者越發現自己也能為非功能性需求寫測試——例如效能,或程式碼是否符合標準。
加瑪(Erich Gamma)創造了「測試上癮(Test Infected)」一詞,形容那些沒有測試就不寫程式的人。
測試告訴你何時完成:測試通過,你這一刻的寫程式就完成了;當你想不出任何可能失敗的測試時,你就徹底完成了。
測試既是資源,也是責任#
你不能只寫一個測試、讓它通過,就宣告自己完成。你有責任寫下每一個你能想像「不會立刻通過」的測試。
過一陣子你會擅長對測試做推理——如果這兩個測試通過,你就能安全地推論第三個測試也會通過,不必真的寫它。
當然,這正是同一種會在程式裡製造 bug 的推理,所以務必小心。如果問題後來浮現,而且原本寫了那第三個測試就會被抓出來,你必須準備好記取教訓:下次就把那第三個測試寫出來。
為什麼測試是必要活動#
大多數軟體出貨時,並沒有伴隨完整的自動化測試。可見自動化測試顯然不是「必要」的。那為什麼作者不把測試從必要活動清單裡拿掉?他有兩個答案。
- 長期理由——測試讓程式活得更久(前提是測試有被執行與維護)。有測試時,你能做變更的期間比沒測試時更長;只要你持續寫測試,你對系統的信心會隨時間增加。
- 短期理由——我們的原則之一是順著人性、不逆著人性。如果測試只有長期論證撐著,那就免談了:有些人會出於責任感、或因為有人在背後盯著而去做;一旦注意力鬆懈或壓力升高,新測試就不會被寫、寫好的測試不會被跑,整件事就散了。所以既然我們想順著人性、又想要測試,就必須找出一個自私的短期理由。
幸好短期理由確實存在:
作者曾在一場公開的程式設計示範中抓到自己這麼做:每次他從面向觀眾轉回來準備繼續寫程式,就按一下測試按鈕。他根本沒改任何程式碼,環境也沒有任何變化——他只是想要一小劑信心,而看到測試仍然通過就給了他這個。
「一邊寫程式一邊測試」也比「只寫程式」更快。 作者一開始並未預期這個效果,但確實注意到了,也聽許多人回報過同樣的事。不測試也許能讓你在半小時內多產出一點;但一旦習慣了測試,你很快就會注意到生產力的差別——而這份增益來自除錯時間的減少:你不再花一小時找一個 bug,而是幾分鐘就找到。
有時候你就是沒辦法讓一個測試通過。那你八成有個大得多的問題,此時該退一步,確認你的測試是否正確,或者設計是否需要精煉。
不過有個危險:測試做得不好,會變成一副玫瑰色眼鏡。 你因為測試全過而獲得虛假的信心,以為系統沒問題,然後往前走,渾然不覺自己在身後留下了一個陷阱——它已上膛,等著你下次路過時彈起。
測試的訣竅是找出你願意容忍的缺陷水準。 如果你能承受每月一次客訴,那就投資測試、改進測試流程,直到達到那個水準;然後,以那個測試標準為準,當測試全過時就當作系統沒問題,繼續前進。
兩套測試、兩種受眾#
往前看一點,我們會有兩套測試:
- 單元測試——由程式設計師撰寫,說服自己程式的行為與他們所想的一致
- 功能測試——由客戶撰寫(或至少由客戶指定),說服自己整個系統的行為與他們所認為的一致
這兩套測試也有兩種受眾:程式設計師需要把自己的信心具體化為測試,好讓其他人也能分享這份信心;客戶則需要備妥一組代表他們信心的測試——「好吧,如果你能把這些案例全算出來,那系統應該就沒問題了。」
傾聽#
程式設計師什麼都不知道。更準確地說,程式設計師不知道任何業務人員覺得有趣的事。(嘿,如果那些業務人員能不靠程式設計師,他們一秒鐘就會把我們掃地出門。)
這是要說什麼?如果你決心要測試,你就得從某處取得答案。既然你(身為程式設計師)什麼都不知道,你就必須問別人。他們會告訴你預期的答案是什麼,以及從業務角度看有哪些不尋常的案例。
而如果你要問問題,最好準備好傾聽答案——所以傾聽是軟體開發的第三項活動。
程式設計師也必須在大尺度上傾聽:
- 傾聽客戶所說的業務問題是什麼
- 幫助客戶理解什麼難、什麼容易——所以這是一種主動的傾聽
- 他們提供的回饋,反過來幫助客戶更好地理解自己的業務問題
光說一句「你們應該彼此傾聽、也傾聽客戶」幫助不大。人們試過了,沒有用。
我們必須找到方法結構化溝通,讓該被溝通的事情在該被溝通的時候、以該有的細節程度被溝通。同樣地,我們發展的規則也必須抑制沒有幫助的溝通——那些在「要溝通的東西還沒真正被理解」之前就進行的溝通,或細節多到反而掩蓋了重點的溝通。
設計#
為什麼不能就這樣:傾聽、寫一個測試案例、讓它通過,再傾聽、再寫測試、再通過——無限循環下去?因為我們知道事情不是那樣運作的。
你可以這樣做一陣子;在一個寬容的語言裡,你甚至可以這樣做很久。但終究你會卡住:讓下一個測試案例通過的唯一辦法,是弄壞另一個;或者讓那個測試案例通過的麻煩程度,遠超過它的價值。熵(entropy)又收下一名受害者。
避免這件事的唯一辦法,就是設計。設計是創造一種結構,用來組織系統中的邏輯。
好的設計:
- 組織邏輯,使系統某一部分的變更不總是要求另一部分跟著變更
- 確保系統中的每一段邏輯都有唯一一個家
- 把邏輯放在它所操作的資料附近
- 允許系統的擴充只需改一個地方
壞的設計恰恰相反:一個概念性的變更需要改動系統的許多部分,邏輯必須被複製。最終,壞設計的成本會變得無法承受——你再也記不得所有隱含連動的變更該發生在哪裡,也無法在不弄壞既有功能的前提下加上新功能。
因此,我們這套新紀律要結構化的最後一項活動就是設計。我們必須提供一個脈絡,讓好設計被創造出來、壞設計被修好、當前的設計被每個需要學會它的人學會。
如後續章節所見,XP 達成設計的方式,與許多軟體流程相當不同:在 XP 中,設計是所有程式設計師日常工作的一部分,就發生在他們寫程式的過程中。
但無論用什麼策略達成,設計這項活動都不是選配。要讓軟體開發有效,它必須被認真對待。
小結#
- 你寫程式,因為不寫程式,你就什麼都沒做
- 你測試,因為不測試,你就不知道何時寫完了
- 你傾聽,因為不傾聽,你就不知道該寫什麼、該測什麼
- 你設計,好讓你能無限地繼續寫程式、測試與傾聽
就這樣。這四項就是我們必須幫忙結構化的活動。