1. 程式人生 > >如何編寫出好的使用者需求文件

如何編寫出好的使用者需求文件

許多軟體開發團隊沒有需求工程師;開發人員捕獲、編寫和管理所有的需求。這在資源效率方面是有意義的:開發人員可以在正式編碼之前,在系統停機時間收集和編寫需求。然而這一做法的缺點是,通常程式設計師沒有在編寫需求方面受過技術和工具的培訓,結果他們總是費力和低效地工作,而且有時做出的需求規約不符合規範。

為了寫出好的程式碼,開發人員必須知道很多事情:諸如控制結構和呼叫約定之類的基本概念;至少一門程式設計語言,包括它的語法和結構;作業系統基礎;以及如何使用諸如編譯器、偵錯程式、整合環境這類的技術。好在他們能以所有這些知識為跳板寫出好的需求來。通過應用許多與他們編寫程式碼時相同的原則和概念,開發人員可以有效地擔當起需求工程師的職責。

讓我們看一些開發人員可以利用的程式設計概念。

遵循結構

所有的程式設計語言都有一種結構。這種結構指出程式的不同部分如何定義,彼此之間形成何種關係。Java程式由類來形成結構,COBOL程式有不同的“區段”,C程式有一個主程式以及多個子程式。

程式有一個特定的結構,需求也是如此。設想你把一個C程式的所有程式碼都塞到主程式裡——它會變得不可讀而且無法維護。與此相似,如果你的需求規約只是一張毫無規則的大列表,你也沒辦法使用它。不管你意識到沒有,一組需求總有一個結構。獲得需求中結構的最佳方法是把它們按不同型別來組織,而這些型別常常對應著不同的級別。

為理解不同型別之間的差別,我們來看看用於保險索賠處理的四條需求樣例:

我們必須有能力處理積壓下來的索賠單據。
系統必須能自動檢查索賠表單以獲得適用條款。
對索賠者,系統要根據其社會保險號碼確定其是否是註冊使用者。
系統要支援處理多至100個併發的索賠請求。
你的直覺或許會告訴你其中每條需求都有一些不同的東西。第一條需求是級別很高的;它在表達業務需要的時候甚至沒有提到系統本身。第二條需求表達了系統應該做什麼,但仍在較高的級別上;它仍然太寬泛,不能直接翻譯成程式碼。第三條是低級別的需求;它確實為軟體必須完成的功能提供了足夠的細節,使你能寫成程式碼。第四條需求雖然非常詳細,但並沒有告訴你係統必須做什麼;它只是規定了系統必須有多快。當你跟使用者和其他涉眾打交道時,這些需求是非常典型的。也許你已經看到為什麼把它們放在一個大而無組織的表上會導致混亂。

為使需求更為可用,你可以把它們按範疇或型別分開,比如:

業務需要
特性
功能性軟體需求
非功能性軟體需求
這是IBM Rational Unified Process(RUP)中建議的型別。它們絕非唯一可能的型別,但它們表達了一種有用的方法。在你的專案的早期,就要決定用什麼型別。這樣,在你從涉眾那裡收集資訊時,要確定他們描述的是何種需求型別,然後寫成需求。

注意你可以用兩種格式之一指定功能性軟體需求:宣告形式和用例形式。上述第三條需求是宣告形式的;它是粗線條的,用了一個“要……”的句式。另一個形式是用例的,它也指定了系統應該做什麼,級別足夠低,能直接寫成程式碼,不過它提供了更多的上下文,告訴使用者和系統應該如何互動以執行一些有價值的東西。(關於用例的更多細節見下文。)在你著手收集專案需求之前,你應該確定哪一類功能型需求是你要用的,以後就不要改變。

用習慣保證質量

你知道寫出好的程式碼和壞的程式碼都是可能的。有很多途徑導致壞的程式碼,其中之一就是使用非常抽象的函式名和變數名,比如routineX48,PerformDataFunction,DoIt,HandleStuff,do_args_method。這樣取名沒有給這些方法和過程的功能提供任何有用的資訊,迫使讀者鑽進程式碼裡去搞清楚。另一個糟糕的做法是用單字母的變數名如i,j,k。你用一個簡單的文字編輯器無法搜尋到這些變數,其功能也不清晰。

當然,你也有很多途徑寫出壞的需求。可能最糟的錯誤是二義性。如果一條需求讓兩個人按兩種方式解釋,這條需求就有二義性。比如,這是一個從實際需求規約裡抽出來的需求:

應用程式在多個併發使用者訪問時必須極其穩定,且不能犧牲速度。
多個和極其這樣的詞有多種解釋,所以這一需求是有二義性的。事實上,為了獲得清晰性,你必須把它表達成這樣三條非常具體的需求:

系統故障的平均間隔時間不能大於每星期一次。
系統應支援1000個併發使用者同時查詢資料庫,而不會發生擁塞或資料丟失。
系統的平均響應時間在多至1000個併發使用者時應小於一秒。
質量需求還有更多的屬性,詳見IEEE的指南。 1

詳細編寫註釋

風格良好的程式包含註釋,這些註釋為程式碼提供了額外的資訊,解釋這段程式碼在做什麼或者它為什麼用這種方法寫。好的註釋不解釋程式碼怎樣做某件事情——這一點程式碼本身顯然已經說清楚了——而只提供必要的資訊幫助使用者、維護人員和複審人員理解程式碼做了什麼,以此保證質量。類似地,需求也有屬性——這是使需求更為可讀和可用的資訊。當你捕獲需求時你也應該尋求屬性資訊。例如,一個重要的屬性是來源:這條需求是從哪裡來的?記下你的資訊的來路將在你需要回溯以獲得更多資訊時節約大量時間。另一個屬性是使用者優先順序。如果一個使用者給你五十條需求,他也應該讓你知道其中每一條跟其他相關的比起來重要度如何。這樣在專案生存週期的後期,時間越來越緊迫,你意識到已經不可能滿足所有的需求時,至少你還知道哪些是最重要的。

正如沒有哪條規則告訴你程式碼裡的註釋一定要怎麼寫才正確,也不存在“正確”屬性的一個普適的列表。來源和優先順序幾乎總是有用的,但你必須定義適合你的專案的其他屬性。當你搜集需求時,試著預計一下當你著手設計系統和編碼時整個團隊可能需要什麼資訊。

熟悉語言

顯然,開發人員必須熟悉他們用來編碼的語言,不管是Java,COBOL,C++,C,Visual Basic,Fortran還是其他什麼語言。為了寫出好的程式碼,你必須瞭解語言之間的細微差別。雖然所有語言裡基本的程式設計概念都是一樣的,但在具體某個操作時它們會使用不同的方式。比如,Java的迴圈結構用“for”;Fortran則用“DO”。C語言裡你以子程式名帶上引數來呼叫一個子程式;Fortran裡你用一個CALL語句。

為了寫好需求你也得熟悉語言。多數需求是用自然語言寫成的(法語、英語等等)。自然語言非常強大,但也非常複雜;未受過寫作訓練的開發人員在寫作中表達複雜想法的時候有時會碰上困難。我們這裡沒有足夠的篇幅留給一個完整的寫作課程,但有些指導原則是有用的。

首先,對宣告形式的需求使用完整的句子。(例如,以“應”或者類似的結構表達的語句。)在每個句子裡檢查主語和動詞。

第二,使用簡單句。一個語句只包含一個獨立子句,只傳達一個想法時,更易於理解、檢驗和測試。如果你的需求太複雜,難以用簡單句表述,試著把它分解成幾條更小、更易於定義的需求。並列句和複合句會引入依賴關係(分支);換言之,它們可能描述那些依賴某些動作的變數,結果常常產生一條不必要的複雜需求,增加測試的困難。

簡單句:系統應能顯示車沿跑道繞行一圈花費的時間。
並列句:系統應能顯示車沿跑道繞行一圈花費的時間,且時間的格式應是hh:mm:ss。(這是兩條需求,一條是功能性需求,指定系統要做什麼,另一條是使用者介面需求,指定時間格式。)
複合句:系統應能在車沿跑道繞行一圈後5秒之內顯示這一圈花費的時間。(這也是兩條需求,一條功能性需求和一條效能需求。)
為了給並列句和複合句寫出合適的測試,你只能把其中的兩條需求分開。既然如此為什麼不乾脆從一開始就這樣做呢?以下是把上述複合句拆分成簡單句的一種方案:

系統應能顯示車沿跑道繞行一圈花費的時間。
繞行時間的顯示格式應為hh:mm:ss。
繞行時間應在一圈結束後5秒內顯示出來。
注意,為使需求更易測試,它們寫得更易閱讀。

還有一個寫好需求的技巧:使用一致的文件格式。你已經有了一個編碼的格式或模板。編寫需求的時候也可以利用它。一致性是關鍵;每個規約文件應該用相同的標題、字型、縮排等等。模板有助於做到這點。實際上,它們起到表單的作用;編寫需求的開發人員編寫好的規約就無需從草稿開始,做重新發明車輪的工作。如果你需要模板的樣例,RUP上面有很多。

需從草稿開始,做重新發明車輪的工作。如果你需要模板的樣例,RUP上面有很多。

遵循指南

許多開發團隊使用類似這樣的編碼指南:

將模組的定義和實現放在不同的檔案裡(C++)。
在一個程式碼塊的範圍內作縮排(Java)。
將頻繁呼叫的資料元素放在每組工作儲存區變數的開頭(COBOL)。
在編寫需求時你也應該遵循某種指南。例如,你如果決定用用例來規約軟體需求,你的指南就應該告訴你如何寫出事件流程。用例事件流程解釋了系統和使用者(參與者)如何通過互動完成工作。你的指南應當描述在主流程裡發生什麼(成功場景),在備用流程裡發生什麼(意外場景),也應該描述如何組織這些流程的結構。你的指南也應該提示兩個流程的長度以及其中的獨立步驟。如果你決定使用傳統的宣告式需求,則指南應當解釋如何編寫需求。好在許多這樣的指南已經在RUP和其他相關的資源裡有了, 所以你不必自己撰寫。 2

理解操作環境

為開發好的程式碼,你必須熟悉那臺執行你的系統的機器,也必須熟悉怎樣使用其作業系統。如果是Windows,你必須熟悉MFC和.Net。如果是Linux,你必須熟悉UNIX系統呼叫。

為寫出好的需求,你要理解的是操作人員而不是作業系統。你也必須理解使用者而不是使用者介面。Java開發人員考慮類路徑;需求編寫人員考慮通向類(或工作組)的正確途徑。

需求捕獲是一項以人為中心的工作。你不能虛構需求,只能從其他人那裡獲得需求。這對內向的開發人員來說或許是個挑戰,但如果能正確地應用已有的技能,他們是能成功的。

使用者常常不知道他們要的是什麼,或者知道是什麼卻不知道怎麼描述它。開發人員卻擁有這樣的能力來改善這一點:他們常常不得不破解編譯器給出的費解和難懂的錯誤資訊。比如,Java開發人員在寫一個小應用程式時可能碰上這樣的資訊:

load: com.mindprod.mypackage.MyApplet.class can't be instantiated.
java.lang.InstantiationException: com/mindprod/mypackage/MyApplet
這是什麼意思呢?如果開發人員不確定,他會去檢查程式碼,查詢編譯器的文件,甚至利用Google這樣的搜尋引擎,來弄清問題出在哪兒。最後他將發現,他寫的小應用程式程式碼缺少預設的建構函式。

如果你正在為一個天氣預報系統收集需求,涉眾之一告訴你係統應能“用標準的帶小尾巴的箭頭顯示200平方英里的一塊區域之上大氣層不同高度的風速和風向”,你就需要深度挖掘一下。你可以要一個類似的系統給出的報告,求助於氣象學的書籍,也可以請另一名涉眾把要求描述得更精確一些。你應當繼續研究,直到掌握足夠多的細節來描述期望的功能。這樣你就可以重述需求,使之清晰和無二義性,提供足夠的細節以支援設計。

另一個捕獲需求的技巧是避免問誘導性的問題。雖然你對使用者的需要可能已經有了想法,但如果你把這些和盤托出,你恐怕無法獲知整體上他們到底需要什麼。反之,問一些開放性的問題如“你要讓分開的資料怎樣顯示?”而不要問“你要不要把氣壓和溫度合起來顯示在一張圖表裡?”

遵循既定原則

設計和編寫優秀程式的基本原則中,有資訊隱藏、耦合和內聚。在編寫需求中也有與之相應的原則。

資訊隱藏

這個原則是說一段程式碼的使用者/呼叫者不能訪問甚至不能獲知資料的內部細節。所有對資料的訪問與修改必須通過函式呼叫來完成。這樣,你在改變內部資料結構時不會影響到呼叫它的外部程式。

對於需求這也是一個好的原則,特別是在用例表達的情形。如我們所說過的,用例具有事件流程。寫得不好的用例常有塞滿了資料定義的事件流程。考慮這個管理購買請求用例的基本事件流程:

基本事件流程:

系統顯示所有未決的購買請求。
每個未決請求包含該請求的如下資訊(限制為char型):
授權ID (僅內部使用)
PO #
引用ID
釋出者帳戶縮寫名
經銷商帳戶名(前10個)
經銷商帳號
購買原因碼
請求購買量
請求日期
分配給內部
註釋指標
經授權的管理員可以做以下幾件事之一:1)批准 2)拒絕 3)取消 或 4)分配請求。他選擇1)批准。
……如此等等直到所有步驟完成。
以上十五行裡,十一行用來說明哪些資料與一個未決請求在一起處理。這些資訊很重要,但卻使用例中發生的事情變得不清晰。更好的解決方案是將資料隱至別處。這些步驟就變成這樣:

基本事件流程:

系統顯示所有未決的請求。
經授權的管理員可以做以下幾件事之一:1)批准 2)拒絕 3)取消 或 4)分配請求。他選擇1)批准。
……如此等等直到所有步驟完成。
未決的購買請求用了斜體,指出資料在別處定義(通常在用例的特殊需求段或在詞彙表中定義)。這使表達真實功能性需求的事件流程易於閱讀和理解。

耦合與內聚

對編碼人員,耦合原則是指程式中的單獨模組應當儘可能地互不相關。 一個模組內部的處理應與其他模組的內部機制無關。內聚原則是指一個模組內部的所有程式碼應該只完成一個目標。這些原則使程式易於理解和維護。

這些原則對需求同樣適用,尤其是用例。用例應當獨立(即極少或沒有耦合)。 3 每個用例應指定一個有意義的功能塊,說明系統如何為參與者提供有價值的東西。參與者焦點很重要,你可以指定系統為參與者做什麼,而不必擔心用例按序排列的問題。

一個用例中所有的功能性成分應當只用於完成參與者的一個目標(高度內聚)。在一個典型的自動取款機(ATM)系統中,一個用例是“取款”,另一個是“轉帳”。每個用例集中於單一的目標。如果你把這些功能合併到一個用例裡,就成為低內聚的(和不合適的)依賴關係。

然而要小心,許多用例的初學者走過了頭,建立了太多的低層用例。我看到過一個用於銀行債務收集系統的模型,它擁有150個用例,用例的起名諸如“修改資料”。這個專案有一個十人團隊,計劃持續了差不多一年。然而,由於這些用例太瑣碎,整個團隊在推進的時候碰上了無數的麻煩。他們大量描述了對使用者毫無價值的底層功能,這些功能既難以理解也難以使用。每個用例極為內聚,但也因此造成用例間的高度耦合。把層次提升到更明確的活動如“收集債務資訊”,就能在耦合和內聚之間保持合適的平衡。