l

2019年3月15日 星期五

落實TDD的三個難題(上):領域模型與軟體架構

March 15 07:11~09:11


緣起

幾年前有一位泰迪軟體忠實學員問Teddy:「我上過TDD的課,但回公司後卻不知道該怎麼落實,為什麼?」當時Teddy無法回答這個問題,因為:(1)他上的TDD課程不是Teddy教的,不知道他學了什麼;(2)Teddy以前工作上開發的軟體,只有不到10%是採用TDD,其他大部分都是用傳統OOAD,code first(先寫production code再寫test code)的方式,所以沒在工作上遇到全面落實TDD的問題。

雖然工作上TDD用的不多,但Teddy寫了很多測試案例,也做了持續整合,算一算也有16年的時間。這幾年因為教學需要,投資大量時間在TDD/BDD/SBE以及DDD/Clean Architecture上面,直到最近才慢慢有種可以清楚回答N年前這個問題的感覺。

***

例子太小

不少人都是透過網路上的例子或是TDD Kata來學習TDD,這些例子大多具備以下特點:

  • 所需物件很少:只有單一或少量物件、例如著名的Bowling Game Kata,只需要一個Game物件就搞定。
  • 商業邏輯明確:上述提到的Bowling Game Kata,或是計算不同方式的郵寄費用(平信、掛號、國內快捷、國際郵件)、商品費用(一般客戶、VIP、大量採購、特價商品)等例子,它們要解決的問題「商業邏輯」都非常明確,很容易透過TDD,採用逐次完成每一個例子的方式來實作完整商業邏輯。
  • 不須考慮架構:因為例子小,邏輯清楚,所以也不需要考慮軟體架構的問題。

以上特性,對於一個「以學習TDD為目的」的例子來說,原本都不是問題,反而是優點。因為例子很專注在特定的小問題上面,所以學習者可以在短時間內把握TDD的精神:

  • 寫一個失敗的測試案例
  • 用「最笨」的方式撰寫程式碼讓測試案例通過
  • 重構程式

***

問題在哪裡?

當使用者學了TDD要實際應用在工作上的專案,此時卻發現,實際要解決的問題放大了N倍。不只物件變多、商業邏輯複雜,連帶著軟體架構也需要一起考慮。這些都是在學習TDD階段沒有冒出來的因素(forces)。

奇怪,為什麼看別人TDD,只要透過撰寫測試案例,物件與介面好像信手拈來就有。換成我來TDD,就D不出來,最後只能XD。


▼如下圖所示,這個問題隨著驗收測試開發(ATTD)、行為驅動開發(BDD)、實例化規格(SBE)等方法,將原本傳統TDD「先撰寫失敗單元測試」提升為「先撰寫失敗驗收測試」之後稍有緩解。驗收測試提供TDD一個更大的Context (背景、脈絡),讓開發人員擁有更多的資訊來「隔空抓藥」,透過測試案例描述物件以及物件之間的互動關係。


▼Specification By Example範例


但就算有了驗收測試,整個系統的全貌還是無法清楚呈現,透過每次撰寫失敗測試案例來完成系統依舊屬於由下而上的開發方式。

由下而上的開發方式最大的問題就是:「最後兜出來的系統很容易長歪掉。」看到這裡鄉民們可能會想:「敏捷開發是一種迭代與增量的方法,這不也是一種由下而上的方式?難道敏捷開發也很容易長歪掉嗎?

沒錯,完全正確。這也是為什麼現在敏捷開發流行採用「影響力對照(impact mapping)」與「用戶故事對照(user story mapping)」協助團隊關照系統全貌。撰寫程式之前,不管這個程式是test code還是production code,如果對於所開發的系統缺少一種「整體的感覺」,最後的系統設計就很容易長歪掉。

***

怎麼辦?

其實答案很簡單,就是準備「剛剛好的事前設計」(just enough up-front design)。問題是怎麼拿捏這個「剛剛好」?

▼如下圖所示,對照OOAD與TDD/BDD/SBE的做法,後者少了強調「建立領域模型」(domain model)這個步驟,讓鄉民以為domain model裡面的物件,不需要特別分析與設計就會自然而然隨著撰寫失敗的測試案例而冒出來。就算是剛開始找到的物件不洽當,反正最後總是可以透過重構來改善設計品質。


能力強者如Kent Beck或Uncle Bob等級的人物,在腦海中已有某種 皇輿全覽圖「軟體全貌地圖」,因此可以信手拈來得到合適的領域物件。就算設計不小心歪掉,後續採用重構來改善系統設計品質對他們而言也不是難題。大師們只需極小化的事前設計便可順利透過TDD完成系統,但一般大眾畢竟敏捷性沒有那麼高,所以適量的事前設計有助於TDD。

扯了這麼久還是沒講到具體解法。以下是Teddy建議的方向:

  • OOAD:如果鄉民們學過OOAD,可以參考80-20原則。找出系統中20%最優先的use case或user story,花一點點時間地建立domain model。有了這個domain model,對於後續將「失敗測試案例」轉成test code會很有幫助。


▼cleanKanban系統的領域模型


  • DDD:參考領域驅動設計方法(Domain-Driven Design;DDD),建立domain model與通用語言(Ubiquitous Language)。有這兩項「致命武器」,後續不管你想「怎麼D」都可以得心應手。


▼透過事件風暴(event storming)找出領域事件與建立領域模型


  • Clean Architecture:軟體架構百百種,屬於「插件式架構」的Clean Architecture,因為具備高度擴充性與可測試性,很適合作為各種軟體的「預設架構」。搭配Clean Architecture,撰寫TDD的失敗驗收測試直接對應到呼叫Use Case,而Clean Architecture的Use Case有著固定的結構,需要定義清楚的Input與Output介面。也就是說,Clean Architecture限縮了開發者的「選擇性」,而讓TDD的開發的工作變得更簡單。

▼搭配Clean Architecture採用TDD開發所撰寫的失敗驗收測試。

***

結論

敏捷開發與TDD都不鼓勵大量事前設計(Big Up-Front Design;BUFD),但並不是說不需要任何事前設計直接「帶著鋼盔往前衝」就可以攻克敵軍山頭。合適的事前設計,可以幫助開發人員釐清目標,支撐後續迭代與增量式開發活動,讓整體設計慢慢湧現。

***

友藏內心獨白:緣分到了,問題就想通了。


廣告

對於Clean Architecture搭配TDD與Event Storming(事件風暴)有興趣的鄉民,可參考泰迪軟體的【Clean Architecture這樣學就會了實作班】,2019年4月份課程已確定開課。

沒有留言:

張貼留言