![]() |
VOOZH | about |
引言
「我希望這裡能這樣……」,「我希望這裡能再增加點東西……」——在軟體開發的世界,我們永遠無法解決的一大難題,是客戶紛繁複雜並且不斷變化的需求。如何把需求映射為最終的軟體交付,是每一個軟體開發方法都無法迴避的核心問題。
領域驅動設計(Domain-Driven Design)通過專注領域核心、建立通用語言等手段,以及聚合、倉儲等戰術模式,很好地達到了去偽存真、化繁為簡的目的,從而在團隊協作、劃分邊界、建立模型等方面呈現出足夠的優勢。但是DDD的實踐應用,非常依賴與客戶溝通的技巧以及對領域知識的掌握。而這兩點,都是需要一些實踐經驗積澱的,所以初入DDD並不容易。
實例化需求(Specification by Example,以下簡稱S&E),是我在學習BDD的過程中接觸到的開發方法。之所以稱其為開發方法,是因為它無法解決如何分析建模的問題,而主要回答了如何梳理用戶需求Specification,並最終將其實現為軟體交付Delivery的整個過程。其擅長的,是捕獲需求、確定驗收標準。
那麼,為什麼我要把DDD與S&E相提並論呢?這是因為,DDD能幫助我們劃分討論的上下文、提取通用語言UL、建立軟體模型,S&E則可以幫助我們梳理討論的場景、驗證模型能否達到交付要求、生成開發的相關文檔。所以我個人認為,這兩種方法能形成優勢互補,幫助我們更容易地開發出「正確的軟體」。
關於書籍DDD之父Eric Evans的《Domain-Driven Design: Tackling Complexity in the Heart of Software》、Vaughn Vernon的《Implementing Domain-Driven Design》、Scott Millett的《Patterns, Principles, and Practices of Domain-Driven Design》,還有Jimmy Nilsson的《Applying Domain-Driven Design and Patterns: With Examples in C# and .NET》為我們提供了完整的理論和具體的實踐。
現在通過我的書摘,再看看
Gojko Adzic的《Specification by Example: How Successful Teams Deliver the Right Software》是怎麼通過正反事例的對比,來逐步闡述Specification by Example這一方法的吧。
註:《Specification by Example》一書已有中譯本《實例化需求:團隊如何交付正確的軟體》,由人民郵電出版社出版。不過我手裡只有這漿糊一樣排版的英文原版。所以,下文完全出自於我的個人理解,各種類比和小結將不斷穿插其中。
軟體開發的壓力主要來源於:時間——開發的期限越來越短,成本——維護的要求越來越高,變化——需求改變的頻率越來越高。S&E採用一系列彼此銜接的處理模式及其產出的工件(artifact),幫助我們順利實現需求。其過程主要包括以下環節,並作為本篇各小節的目錄:
目前實例化需求的過程現在有兩種流行的模型:以驗收測試為中心的ATDD,側重於自動化測試,優點在於使開發目標更明確,並防止功能退化;以系統行為規範為主導的BDD,側重於制定系統行為的場景,在客戶與開發團隊之間建立共識。這兩種模型,各有長處、各有用途,所以無所謂孰優孰劣。我們關注的,是它們生成的活文檔,這是實例化需求產出的最好工件。書的第三章,率先回答了什麼是活文檔的問題,並倡導建立「以文檔為中心」的理念。
維護開發文檔總是一件費力不討好的事情,卻又總是不得已而為之,因為將來的重構與維護都需要以這樣的一份文檔為基礎。這份文檔需要能快速地勾勒出系統的輪廓,清晰地表達出系統主要的概念,準確地描述系統架構和模型的結構。最關鍵的,這份文檔必須是最新的。所以,這份文檔不僅要完整,還必須是「鮮活的」。
自動化測試本身是按一定的邏輯編排的,所以具有一定的組織結構性。測試方法的名稱,也可以看作是測試的一種「自描述」文本。所以這種結構性與自描述性,與開發文檔的需求不謀而合,所以測試可以被當作文檔的一種形式——「代碼即文檔」。
但是要注意,不能因此偏重於測試本身,而忽略了測試與需求之間的聯繫,使得測試變得臃腫和不易修改。ATDD的方法,往往過於注重編寫和執行測試,因此容易寫出不易維護的測試,導致有需求變化時,產生牽一髮而動全身式的連鎖反應,大量的維護與重構工作使得先前的測試工作變得得不償失,因此要極力避免。
如果把帶有Example的Executable Specification比喻為頁面,那麼整個活文檔就是由此構成的一整本書。利用Relish等BDD工具,我們可以把通過自動化測試驗證後的Specification提取為HTML或者PDF等格式的文檔系統,這甚至可以稍加修改就作為用戶手冊使用。
綜上所述,因為重構與維護的難度,我們需要一份組織良好的文檔。BDD的自動化測試正符合文檔的要求,而且恰好這種文檔可以利用一些BDD工具從可以執行的Specification中提取出來,所以實例化需求方法可以視作一種建立在「以文檔為中心」理念上的開發方法。
根據業務目標劃定問題域敏捷開發是以用戶故事為核心的,所以故事講得好不好至關重要。那麼這個講故事的責任究竟應由誰來承擔?傳統的軟體開發,認為劃分問題域、講清故事是客戶的事。對此,《BDD in Action》和本書的兩位作者都反覆強調,『不能交由客戶去編寫用戶故事、用例清單等細節,否則就等同於讓客戶去提供一個具體的、高層次的解決方案了。』所以,劃分問題域、講好用戶故事,是開發團隊的責任。在劃定問題域這個環節,重點應該是引導用戶弄清究竟需要什麼,進而通過發掘現有業務的潛在,提出新的思路和新的方案。
劃定問題域的具體方法,是理解「Why」與「Who」。這和我在前篇對結合BDD進行DDD開發的一點思考和整理中介紹Impact Mapping這個工具時一樣,重點是理解「為什麼要這樣做?」、「誰人將從中受益?」等問題,弄清客戶開發系統所能期望的價值究竟是從何而來。此時,由於系統輪廓不清不楚,可能會感覺無從下手。為此,建議先分清業務目標與要交付的功能,而不是嘗試去把每一個用戶故事描述清楚。對此,我個人認為Impact Mapping提供的Goal-Actor-Impact-Deliverable模型,將是一個非常合適的挖掘工具。我們可以通過連續的Why提問,來弄清真實的、具體的、有期限的、能度量的業務目標。
當難以確定業務目標時,先不要急於討論需要哪些功能,而是可以從描述客戶期待的輸出入手,分析為什麼需要這樣的輸出,從而歸納出業務目標所在。比如對於一個ERP系統,堅持「Report-first」,用各種報表展示客戶期待的系統輸出結果,由此發掘業務目標,可以幫助我們把注意力集中在具體的報表項內容上,而暫時把流程、處理等功能性需求放在一邊。
這樣三段式的描述,可以與Impact Mapping的內容有機地聯繫起來,相當於根據Actor-Impact-Deliverable直接轉譯而來。
系統需求,需要由客戶與開發團隊達成一致,確保系統的各個方面的功能都被包括其中,並有明確具體的驗收指征作為約束。這和DDD中分享消化業務知識,得到領域的通用語言是一致的。在DDD中,也提倡專注於最有意思的對話上,並從用例開始,從一個系統行為作為起點,組織開發人員、業務人員和業務專家,圍繞一個特定的場景進行討論,由此發現這一場景內的領域概念和業務知識。這一點也正是我非常珍視的,實例化需求和BDD這一類方法,楔入DDD的關鍵點。
這種協作,不僅發生在開發人員與客戶之間,同樣也在開發人員與測試人員之間。如果開發人員與測試人員沒有圍繞Specification達成一致,那麼雙方就會各行其是。開發人員看到的是一堆的需求,而測試人員看到的是一堆的測試用例。若由開發人員撰寫Specification,它會因為過於貼近模型設計而充斥大量的模式、架構元素,從而變得難以理解。若改由測試人員獨立撰寫時,可能又會因為太過瑣碎零散而變得難以維護,最終迷失在各種測試細節的汪洋大海之中。測試人員編寫的測試,沒辦法幫助開發人員去組織整個系統的各個部分,也無法通過自動化測試驅動整個開發過程。測試人員編寫的測試,也沒法被當作Specification再被開發人員利用,因為這些測試都是站在測試人員的立場,用測試人員的方言、專業術語編寫和描述的,所以沒辦法用於雙方的溝通。對於測試人員,則會在每次系統需求改動時,面對一大堆的測試重構。因為這些測試都不支持自動化測試,或者不容易被其他人理解。所以,協作是廣泛的、多重的、具體的。
視協作的規模不同,可以分為:
只有當場景描述具有很強的帶入感時,才能激發客戶參與討論的熱情,才更容易達成共識,並發掘潛在的概念和需求。傳統的基於平面文檔的平鋪直敘的方式,在向用戶展示系統場景、捕捉系統需求時,可能會因為詞不達意,而導致不同的人產生不同的理解,所以描述性的文檔始終無法與清晰的代碼媲美、也遠沒有代碼直接。然而清晰的代碼並非一朝一夕,讓用戶直接面對系統代碼也完全沒有意義,所以我們退而求其次,改用一個特定場景下的具體事例來表述系統的行為,達到與客戶有效交流的目的。所以,舉例說明的方式,對於共同認識和理解某個場景是非常有益的。
在選擇和描述每一個例子時,作者提出要堅持「例子四原則」:
例子總是明確的。
例子總是完整的。
例子總是現實的。
例子總是易於理解的。
在安全、性能等非功能性的需求方面, 當其重要性已經達到影響業務價值或者業務目標實現程度的時候,那就清晰地表達出來。這與《UML精粹》中的觀點是一致的,一切需求的重要性視其對實現業務價值、業務目標的影響程度而定。在性能、響應時間這些無法準確表述的需求方面,作者引入了QUPER模型。對這個模型,我個人理解是預估這些指標對應的障礙,以及由此產生的開銷,然後再展開討論。每個問題會被分成三個方面:
作者使用了「手機開機速度」作為例子:不能開機,手機就沒用;開機速度太慢,就沒市場競爭力;開機非常快了,再快就沒有意義了。
提煉需求好記性不如爛筆頭。交流的結果一定要以某種形式記載下來。原始的例子就象未經雕琢的鑽石,只有提煉後才是關鍵的、易理解的、方便轉換為可執行Specification的、能予以自動化測試的Key Example。
這一點,和Example的」明確的」是同樣的含義。要儘可能消除描述上的模稜兩可,並且要保證所有參與討論人員認識上的一致。
腳本通常更側重於描述一個事物是如何變化的,更多的傾向於流程方面的內容。這也是客戶在描述的時候,容易掉入的一個陷阱——「先這樣,然後那樣,接著再怎麼樣,最後又是什麼樣」。這種腳本或者說是流程形式的表述,缺乏一個系統的視角,缺少對系統與用戶交互情況的表達,而且流程本身很容易改變並且難以維護,所以並不適合直接作為Specification。正確的方式,應該按」在這樣的情況下,系統會做出那樣的反應「的形式進行表述。重點是」系統應該做什麼「,而不是」系統應如何工作」。
Specification不要與代碼、與UI等技術實現細節耦合太緊。技術層面的難題,以及流程等細節,留待再下層的自動化測試去解決。
為了提高Specification的可閱讀性,可以給它增添一段描述文本,然後交給其他人看,靜靜地觀察對方的反應。如果對方無需額外提問就能理解並達成一致,那說明這個Specification符合預期。否則就把回答對方的解釋,也寫進開頭的這段描述性文本里。
在篩選關鍵事例時,應優先把握所有成功的場景,而把可能失敗的情景先放在一邊,由簡入繁地先把功能正常地展現出來。在具體篩選時,可以從以下幾個方面著手:
使用Given-When-Then三段式表述Specification,並且儘量避免考慮動作或者事件之間的依賴關係,最好就專注於一個動作、一個事件。對於新舊數據共存的系統,比如ES+CQRS里新舊版本的領域事件,為了保持專注,建議把這種兼容性問題壓入自動化測試層去解決。對於系統當中的預設值,雖然能讓Specification更易讀,但考慮到這種預設值如果表述在Specification中,將會導致過強的依賴性,所以也建議移入自動化測試層。在這個問題上,可以參考「魔數」。當我們把魔數顯式地定義出來時,才更有助於我們消除誤解。將其移動到自動化測試層,或者放入一個全局的配置當中,都是相對更靈活的方法。
這一點又轉回到DDD了,即Specification中引用的概念、關係,都應該與當前上下文中的通用語言保持一致。
用自動化測試驗證需求隨著軟體規模的逐漸增長,測試的數量、大小、應對變化的能力,都要求我們採取自動化的測試方式。在這個過程中,必須以預定的Specification不再修改作為前提,否則我們的自動化測試只能是以訛傳訛了。換個角度看,雖然自動化測試增加了學習的成本,要引入額外的BDD工具,編寫額外的Feature與Specification,得到的卻是前後一致的需求表達和更加輕鬆的後期維護,代碼的實現也會更自然。這一點,我認為和DDD里的從UL到代碼的展開是一致的。因為有統一的UL和清晰的模型,所以直接映射到代碼也就更直觀和自然了。
在具體實施自動化測試時,應該由簡入繁、從易到難,事先做好規劃。因為構建整個自動化測試的上下文環境是相對比較耗時費力的,這個上下文還要集成到一定的系統環境中才能執行,將來需求發生變化時這個環境也能被重用,所以非常有必要在對待整個測試環境規劃時更慎重一點。
在具體的實現環節,將自動化測試與編寫業務代碼同步,比如採用TDD的方法,能讓所有人把精力集中在測試上,保證Specification順利實現。這就如同公交車,如果中途沒有乘客上下,自然會跑得很快。但事實並非如此,測試的職責就是保證Specification的實現、業務代碼的正確,所以自動化測試的規劃與實現,必須要由開發團隊承擔起來,不能交給其他人。
手動測試與自動化測試的區別在於,手動測試側重於準備上下文,然後測試是否通過,關注的是成功與否;自動化測試則關心導致測試失敗的原因。特別的,手動測試通常是腳本化的,一個步驟緊跟一個步驟,每一次測試都重複這個過程。如果測試失敗,那麼手動檢查其中的每個步驟也在情理之中,反正都是手動的。自動化測試則必須消除這種測試步驟之間的依賴性,否則當測試失敗時,無法確定究竟是哪個步驟出了問題,自動化測試將因此退化成手動測試。如果遇到這種情況,可以將其切分為若干個小的測試,比如每個步驟對應一個小的自動化測試,改由測試上下文通過準備測試條件將這些小測試聯繫起來。由此引申出一個問題:測試代碼要不要良好的組織與設計?答案是肯定的。因為良好編碼的測試代碼,才能方便維護和閱讀,並作為活文檔的提煉來源。
Executable Specification通常是用文本或者HTML格式進行描述的(想想Cucumber的Step或者Spock里的測試方法的描述式命名)。這樣當這個可以執行的需求說明發生改變時,通常不需要重新編譯業務代碼。而自動化測試是代碼,並負責對Specification的驗證,所以當需求說明改變時,要重新編譯。此時,為了避免在Specification中混雜太多計算、判斷的邏輯,要把這些驗證邏輯放在自動化測試里,而不要表述在Specification里。因為對於Specification而言,在轉換為Executable Specification時應當關注的是「測試什麼」,而把「如何測試」的責任交給自動化測試。
儘管測試不能是腳本,容易變化的流程應儘量放在自動化測試層。但是無論怎樣,業務流程總是客觀存在的,通過When-Then的事件驅動方式,我們可以藉由若干個Specification的組合,展示完整的業務流程。而具體的業務邏輯,則放在Specification里。所以,不要在測試代碼里重複業務流程或業務邏輯。
依賴UI與資料庫的測試,是自動化測試面臨的最大困難。書里提到了許多建議,但都非常需要實踐進行驗證,才能深刻領會。至少我只理解了皮毛,所以這一段的內容暫時沒有辦法總結。儘管如此,大量地引入Stub與Mock進行測試、隔離UI與業務模型、進行持久化無關的設計、建立統一的應用服務層、在Specification里竭力避免引入UI與存儲相關的元素等等,都是可行的方案,類似MVC、MVP、MVVM等模式也將成為我們解耦的利器。
即使要對UI進行自動化測試,也建議使用針對UI編寫的Specification進行驗證,而且不要使用「錄製-回放」工具。因為這類工具生成的腳本通常會增加一定的學習成本,而且會非常難以理解,不便於維護。
從需求說明到UI的自動化測試,可以從以下3個抽象力度逐漸弱化的不同層次進行實現。其中,需求說明應該在第一個業務規則層中描述,自動化層則應該通過組合第三個技術行為層來表達第二個業務工作流層。這樣分層實現的從需求到測試的描述,更易於理解,也更高效。
用資料庫作為自動化測試的數據來源,是一個比較便利的方式,但是如何管理這些數據卻成為一個難題。要避免直接使用舊存的數據作為測試的數據源,因為這種數據與現在的需求可能存在衝突、不易理解。對於構造過程相對複雜的對象圖,可以嘗試在資料庫里預置相關數據,以提高自動化測試的速度。
在重構需求的同時,頻繁地進行同步驗證這一部分的內容,與重構、持續集成緊密相關,因此提到的也多是化整為零、「不要想一口吃成一個胖子」、用Mock隔離故障點、以事件驅動測試、先保證同步再嘗試異步測試等等建議,並且提倡引入並發測試、快慢分組等方式儘快提高測試的反饋速度。
對於那種周期性執行的測試,引入自然時鐘顯然是不合適的,所以新增「業務時鐘」的概念去控制這個周期,使之可以根據測試要求隨時執行這一類的測試。我的理解,它等同於一個虛擬的時鐘,基本原理還是觸發一個時間事件,然後利用這個事件去驅動測試。
提取活文檔由於提取活文檔更多的是BDD工具的使用,所以只要有編寫優雅的Specification和自動化測試作為基礎,文檔的生成是一件水到渠成的事。在這個部分,主要的建議包括:
實例化需求的實踐,重點和難點都在其中的4個環節:制定需求、描述需求、提煉需求和自動化測試。而《實例化需求》這本書本身的內容,也更偏重於用正反兩方面的具體事例來引導我們的思考,涉及具體操作步驟的內容相對較少。所以和DDD一樣,掌握實例化需求的方法,也需要大量的實操和經驗積累。整理出這篇書摘,附上自己閱讀時的註解,希望可以拋磚引玉、溫故而知新。