第一部分:軟體開發流程概要-《Head First OOA&D》讀書筆記(1)
在第一部分中 (http://www.cnblogs.com/lihongchao/archive/2008/01/12/1036586.html), 簡要的介紹了軟體開發的一個典型過程,也就是所謂的生命週期. 要做好一個軟體項目,這裡的各個步驟都是必不可少的,並不是說這些步驟本身不可少,而是其中所 包含的具體工作肯定都會被考慮到,不管是有意的或者是無意的. 話說回來,要想能夠出色的完成項目,光是盯住這些泛泛的步驟肯定是不行的,決定項目的成敗關鍵在於我們在每一步所做的工作是否能夠為下一步工作提供準確的,充分的資訊和資料。 當我們做砸了一個項目回頭來分析的時候(如果你還有這個機會的話J),你很有可能發現出問題的地方不是缺失了某一個步驟沒有做,而是某一個步驟沒有做得足夠好,或者說是做錯了方向。比如,需求說明文檔描述的需求根本就不對,設計文檔不合理,FixBug太草率,單元測試流於形式等等。記得看一本叫做《人件》的書上講過,“沒有計劃好過於有個錯誤的計劃”。一個錯誤的計劃會麻痹真箇團隊,讓所有人員認為這個計劃是合理的,遵循這個錯誤的計劃,以至於會一點點的把項目的開發引到向失敗的結局。所以我們首先要確保我們所做的工作是基於正確的理解,朝著正確的方向在走,才有可能取得預期的結果。首先我們就討論一下如何正確的走第一步,需求的收集與分析。
各個項目都做需求管理,最終的目標只有一個,就是用一種簡潔,準確的方式表達出項目的真實需求。然後能夠方便的向電腦語言轉變這些需求。到底怎樣才能達到這個艱巨的目標呢?不同的項目特點我們採取的手段也會不同。為瞭解決需求問題所採用的手段對整個開發過程都是關鍵的,最顯著的就是項目所採用的開發流程是Waterfall 或者是Iterative, 再或者是SCRUM。其實需求的情況就是決定開發流程的一個關鍵因素。我們的目標就是開發滿足使用者期望的軟體。因此我們需要知道使用者的期望,那我們最開始需要做什麼呢?
1. 願景, 談話, 功能列表(Vision, Discussion & Feature List)
我們先不說我們需要先做什麼,想一下,一個新項目是怎麼開始的。大多數項目的開始都是很倉促的,領導或者是使用者或者是其他的什麼能指使你的人會給你一份一張紙的所謂需求文檔,也有可能是上百張紙的文檔,但其中有價值的內容一般也就一兩張紙的樣子,比書中的這個例子好不到哪裡去。
Figure 1 願景
這個東西是需求嗎?也是也不是,只能說他是一個不完整的需求。其實這個只是使用者的願景,就是說只描述了一些支離破碎的激動人心的功能,而對於這些功能的使用細節和這些功能的聯絡卻沒有足夠描述。 讓我們按照這個來開發一個系統,肯定是無從下手。在我們把問題瞭解的充分清楚之前,要盡量減少後續工作。比如,在需求設計不清晰的時候,要盡量把開始寫代碼的時間拖後,因為你一旦開始構建系統,就說明你要再做任何修改都要花不小的代價了。那我們怎麼做才能使需求清晰呢?交流!不光在項目開始的時候,在開發的全過程,客戶的參與都是使項目成功的關鍵因素。但是在項目初期,交流的方式比較單一,一般就是拿著紙筆和客戶面對面的交談,儘可能詳細記錄客戶的各種表達出的資訊。對於不是很成熟的客戶,我們很難把這種交談深入下去,因為讓使用者憑空想象他們所需要的系統是什麼樣子確實不是一件容易的事情。有的時候,需要藉助一些工具來啟發客戶,現有系統啊,存檔的文檔啊。一個常見的擷取深層次需求的方法就是構造原型(Prototype),讓使用者有的放矢,這樣能找出比較細緻的需求。需要注意的一點就是,這個原型被構造的目的就是為了清晰我們的需求,沒有其他任何作用。在需求告一段落的時候,原型就應該被拋棄,換句話說,原型就是一個最終要被廢棄的產品。所以,我們開發原型的重點就是快速,準確的把我們理解的一些需求“實現”出來,供使用者評判。我們開發原型的技術完全可以和以後開發系統的技術不同。比如現在很多公司都用Ruby來構造Web的原型,然後用DotNet或者Java開發系統。
如果有的需求比較抽象,不好描述,我們可以採取一些見解的手段,比如異同性分析(Commonality and variability),也就是描述,這個系統是什麼,有那些類似的系統,功能類似,業務類似,甚至介面類似都可以。還要知道,不是哪樣的系統,不包括什麼功能,不需要考慮哪些因素,這樣就把系統所需要涉及的範圍(Scope)很好的定義出來了。
交談結束後,我們要把得到的所有的需求再和使用者確認,無誤後存檔,最好能夠按照某種方式進行分類,以方便以後的查閱。這麼多紛繁複雜的需求怎麼去用?這就是個技巧問題了,其實在這個階段,我們只需要把其中最重要的部分識別出來,所謂最重要,就是說如果我們的系統不實現這些功能那我們的系統就無法實現它的價值。我們要記住,要盡其所能把細節的問題退到最後再考慮,不然會使我們在整體的分析上遇到困難。其他的需求也是很重要的,但一般都在以後的工作中才能體現出價值,比如兩個程式員關於功能實現意見不和的時候,我們就可以在這些需求記錄中尋找相關記錄,得到正確的方案。目前我們能夠得到的是一個功能列表,能夠按照優先順序,依賴順序排列,輔以簡要介紹。這個功能列表要用客戶熟悉的詞語表示,然後確認這就是使用者想要的東西。如果能把這些準確完成就算很成功的前期工作了。要注意一點,這裡講的功能(Feature)和需求(Requirement)是沒有明確的區別的,一般認為,功能更抽象一些,一般一個功能覆蓋了,滿足了多個需求,也可以把功能,這種更抽象的需求,依然叫做需求,像一樣。
Figure 2 Feature List Sample
功能列表只是描述系統的一個角度,表示系統要具備哪些功能。另一個角度,就是使用者能夠怎樣使用這個系統,就是我們常說的使用案例圖(Use Case Diagram),這兩種角度的文檔相互對照,能夠確保我們得到了完整的需求。
2. 實際情境(Real world)
我們的軟體最終會在現實環境中使用,而不會永遠運行在我們的開發與測試機器上,所做的操作也遠遠比我們的測試案例豐富的多。所以我們進行需求收集的時候,就要對於現實環境的一些情況要做足夠的考慮。比如,不同使用者的操作習慣, 使用者的網路情況,使用者的知識水平等等。很大程度上,我們是通過實際情境的想象和類比來找出,我們需要處理的特殊情況,這些特殊情況出現的機率一般不高,但是對於系統的穩定性和客戶的印象非常重要。只有預測並解決了在實際情境中可能遇到的情況,才真正的是站在客戶角度,而不是開發人員的角度,才能使使用者滿意。比如,在Gmail中,當你刪除一封郵件以後,頂端會出現提示條。
Figure 3 Undo Option
讓使用者有機會能夠撤銷所作的操作,這樣當有需要的時候,使用者會非常高興,他們會認為這個系統真正為他所想。如果沒有這個功能,對於開發人員完全可以把無法恢複的責任推給使用者,是使用者操作錯誤,進行了誤刪除。這種解釋在相互的指責中可能會佔上風,但是最終是沒有滿足使用者真正想要的產品,這個需求。所以在需求收集,系統分析以及編碼和測試的時候,都要考慮到,我們的產品最終會被安裝到客戶服務器上,會被真正的使用者使用的。因此,我們要儘可能的把我們置身於使用者的角度,在真正的實際情境中考慮我們的需求,設計以及開發的系統。這部分需求,一般客戶比較容易忽略,因為他們認為是理所當然的,而對於開發人員確實聞所未聞的。
3. 問題分解和用例(Problem Break Up & Use Case)
按照終端使用者的分類,系統被使用的不同情境,以及系統的功能列表,我們可以把整個的系統進行邏輯上的模組拆分,把一個系統拆分成多個小的系統。這裡的拆分僅僅是基於被使用的不同情境,拆分的目的是,使在每一個情境下,系統被使用的流程,以及用來實現的目標都比較單一,容易進行分析。
針對每一個系統被使用的情境,我們都要細緻的進行對於需求的學習和理解,確保我們掌握了正確的需求。然後就可以詳細的描述這種需求了。對於每一個使用者的完整操作,都是一個相互獨立的使用系統達到一個有價值目標的操作組合。比如,我需要用ATM機取款,我需要使用手機發簡訊。這些單獨的應用情境就是用例(UseCase),也有叫做Story的,基本上是指的相同的東西。把系統能夠滿足使用者的所有的這些有價值的操作組合全部整理出來,我們就得到了一個系統的用例列表,一般的講,我們開發出的系統能夠協助使用者實現這些目的,並且使用者取得了預期的結果,那我們就算交付了一個成功的系統。這個用例列表的特點是,我們沒有用任何技術詞彙,通過文字和圖形來表述需求,因此使用者能夠準確的理解它。這樣,我們就可以和確定,使用者所需要做的事情都在這個列表中涵蓋了。更形象的表示方法是畫出使用案例圖(如下)。
Figure 4 使用案例圖
使用案例圖就是我們系統的一個藍圖(BluePrint),形象的描述我們的系統能為使用者做什麼事情。圖中的小人就是指的使用者(使用者不一定是人,也有可能是其他的系統),圖中的橢圓指的是,系統中需要實現的一個個使用者的操作集合。我們就把這個橢圓叫做用例(Use Case)。用例描述了系統為了為使用者實現的一個目標所做的一系列操作。一個完整的用例需要包含三個內容,一是用例要有清晰的價值。二是用例要有開始和結束點。三是,用例要由外部的實體觸發開始。一個用例常常反映多種執行的順序,通常有一個主要的執行路徑和幾個次要執行路徑。次要執行路徑只有在特定條件下才會使用。每一種執行的順序,都叫做一個情境(Scenario). 這些情境都實現了一個相同的目標,也就是這個用例所體現的價值。我們使用用例的方式來描述需求的時候,一定要確保我們已經理解了需求,要多和使用者交流,避免想當然的單獨進行用例開發,而不和使用者確認。用例可以用很多種方式描述,但目的都是要把使用者使用系統的方法盡量表述全面,簡潔。
文本描述,優點是易讀,明了,但對於迴圈和邏輯分支的情境表述困難。
Figure 5 Use Case Sample
圖表方法描述用例,可以非常清晰的描述系統操作的先後順序以及各種條件判斷。
Figure 6 Sequence Diagram Sample
當我們把所有需求都採用某種表現形式描述為用例的時候,我們就會發現一些隱藏的,我們沒有注意的, 或者是使用者沒有告訴我們的需求。所以用例的一個價值就是協助我們發現,系統管理使用者的需求,而且很容易和使用者就此進行交流。用例和功能列表也是有聯絡的,我們的用例必須覆蓋所有的功能列表中記錄的內容。這樣,我們通過和客戶交談得到的功能列表就能夠協助我們來驗證用例是否完整。實踐中,有隱藏的用例沒有被發現的情況是常常出現的。相反的,我們的功能列表中的功能不一定都能夠找到一個對應的用例來使用這個功能,這是因為,用例描述這個系統如何被使用的,至於系統內部的調用關係,就沒有清晰的表示了。由此可以看出,用例和功能是相互協調的,但是他們不是一回事。
有一種開發方式叫做情境驅動開發(Scenario Driven Developing),顧名思義就是一次鎖定一個情境進行開發,使系統能夠成功的執行這些操作。系統實現了這個情境以後,再挑選另一個情境進行實現,一直到所有的情境都實現後,開發工作也就結束了。也會被叫做用例驅動開發。可以看出情境驅動開發的著眼點是使用者的需求,是實現一個個功能背後為使用者解決的問題。這個出發點非常重要 ,不然的話,儘管實現了很多很酷的功能,介面也很漂亮,可到頭來發現使用者根本就不喜歡,也就沒有什麼價值了。
還有一種開發方式叫做功能驅動開發(Feature Driven Developing),也就是通過對功能列表中的功能進行細分(Break Down),然後挑選一個功能點進行開發,把系統中用到這個功能的地方都找出來,實現這個功能以滿足所有潛在的要求。然後再進行另一個功能的實現,以此類推,實現了所有功能也就實現了整個系統。
這兩種開發方法沒有優劣之分,只有不同的適用情境。一般來說,情境驅動的顆粒度比較大,功能驅動的顆粒度比較小。也就是,一個系統分解成的情境數量要比分解成的功能數量少很多。如果拿軟體開發比喻成畫畫,拿開發一個情境像是畫一條完整的線段,不斷的實現一個個情境,畫面上的線條就越來越多。一個功能更像是一個點,不停的實現功能,畫布上的點也就越來越多,最終形成了完整的畫卷。
4. 領域分析(Domain Analysis)
有了完整的用例了,我們就可以進行領域分析了,也就是通過對現有系統,以及使用曆史的研究,基於業務情境內的潛在知識,技術的瞭解,常常在領域專家的參與下,進行的相關知識,資訊的收集,識別,表現的過程。因為這個過程重點是準確理解使用者的目前商務邏輯,相關業務知識,所以非常需要使用者的介入。因此,就需要用使用者看得懂的語言來描述,我們的系統。經常見人說使用者什麼也不懂,一問原來,是把一些UML類圖,模組劃分,甚至代碼給客戶看,讓客戶確認這是他們需要的,這不是自討苦吃嗎。
分析的目的就是藉助使用者的知識來把系統進行模組劃分,這個模組劃分其實就是把系統由使用者的角度轉向開發人員的角度的變化。使用者只知道一個個的功能點,需求。而開發人員要知道,我們的系統的模組,資料庫怎麼組織,商務邏輯如何封裝,使用者介面怎麼表現,系統異常如何處理等。經過領域分析,我們基本就會確定一種系統架構的模式,是三層架構,MVC等等,甚至還會對一些具體功能實現有些想法,比如用哪個設計模式來解決等等。採用哪種系統架構,都是通過對這些需求,用例的分析而最後得出的,而不是一味的追捧潮流。話說回來,我們把系統細分成模組的目的就是,把一個複雜的系統分解成多個單獨的簡單的系統,一個個實現這些簡單的系統,最後我們就會發現,這個複雜的系統就完成了。劃分模組的原則和我們系統設計或者說是OO設計的原則類似,都是把相同屬性,類似行為的需求,功能劃分到同一模組中,盡量使模組之間的關係減少,減少模組之間的依賴關係。
對於一個個的小的模組,我們就可以比較輕鬆的使用物件導向的分析,設計方法來解決了。就拿圖5的用例的例子來說,我們先找出來文字中的名詞,這些名詞都可以看成潛在的類或者叫做實體,對象。當然不能太僵化,有的名詞不在系統中,比如Actor,有的名詞不需要單獨的類。再分析動詞,這些動詞都是主語所表示的實體類的潛在方法或屬性。再運用一些OO的原則,對這個分析結果進行完善,把變化部分的隔離開,封裝起來。把互動的部分,抽象成介面。最終就會產生一個重要的設計產品類圖(Class Diagram),我將在下一篇中詳細講解這些OO的分析和設計的一些原則和方法。
Figure 7 Class Diagram
5. 迭代(Iteration)
從上文能夠看出來,我們通過對一個個較小的模組分別進行分析,設計,編碼,測試,整合這幾個步驟,最終完成整個系統的開發。我們可以吧不同的模組劃分到幾個迭代中進行開發,這樣我們就能夠在較短的時間內,完成一次迭代,有一個基本可以使用的系統供使用者測試。確保我們的設計和實現是使用者真正需要以後,再進行第二次迭代。和使用者的設想有偏差的時候也好儘早進行溝通,對系統進行調整。這樣就可以以較小的代價,逐漸的使需求清晰,滿足使用者。而那些模組放在第一次迭代,那些可以放到後邊再實現,也是有講究的。一般情況下,要著重考慮的就是哪些模組的風險最大,我們的理解最模糊,對整個系統的影響最大,我們就要首先解決這些模組。我們的目的就是不斷的降低不確定性,減小開發的風險。在下文中我將詳細介紹一下,怎樣通過OO的分析來把這些充滿不確性的需求逐漸轉變成,能夠操作的,具體的一個個類。
最後,留個作業,呵呵。討論一下,採用這種迭代的開發方式,以及現在比較流行的敏捷開發,有哪些弊端?如何解決?提示一下,和使用者相關。