連載:麵向對象葵花寶典:思想、技巧與實踐(26) - 類模型三板斧
類模型設計其實就是程咬金打天下 -- 三板斧 而已 :)
第一斧(照貓畫虎):領域類映射
麵向對象類設計首先要解決的一個問題是:類從哪裏來 ?
有的人可能會認為,要發揮想象力、創造力。。。。。等各種“力”——這種方法的主要問題是:我們不是在進行純粹的藝術創造,而是要最終滿足客戶需求,而不能天馬行空。
還有的人幹脆就說:拍腦袋吧,憑感覺吧 —— 這種方法的主要問題是:猴子能敲出莎士比亞全集麼 ?
看起來以上方法都不太可行,那究竟如何才能從哪裏找到我們需要的類呢?
我們將上一章中的領域模型圖拿出來,重新再看一下:

相信不用我多說,絕大部分同學一眼就能看出:哇塞,這不就是類麼?
1)軟件類來自領域類,領域類來自用例,用例來自客戶,這樣一環扣一環,軟件類的正確性得到了保證,不用擔心拍腦袋帶來的問題;
2)領域類到軟件類的轉換非常簡單,不需要天才的創新,或者豐富的想象力,隻要掌握基本的麵向對象的知識就能完成,菜鳥也能做設計;
3)不需要參考其它係統,不用擔心沒有參照物時無法設計的問題;
從領域類到軟件類的轉換操作非常簡單,基本上就是一個照貓畫虎的過程。
【類篩選】
雖然我們說從領域類到軟件類是一個照貓畫虎的過程,但並不意味著將領域類全盤拷貝過來即可。主要的原因在於“軟件類”是軟件係統內部的一個概念,而領域類是業務領域的概念,並不是每個領域類最終都會體現在軟件係統中。
以POS機的領域類為例,領域類“顧客”不需要轉換為軟件類,因為顧客是POS機業務領域的一個重要參與者,但並不是POS機內部需要實現的一個實體,在POS機業務中,顧客甚至都不是和POS機直接交互的實體,站在POS軟件係統的角度來說,顧客和POS機其實沒有任何關係。
對於屏幕、鍵盤、掃描儀這些輸入輸出設備,一般情況下我們認為它們是POS機係統硬件的一部分,而並不是POS機軟件係統的一部分。但假如POS機有一個需求是既支持圖形界麵輸出,又支持字符界麵輸出,那麼POS的軟件係統就需要處理這種和屏幕相關的需求了,此時屏幕就是POS機軟件係統的一部分了,需要將領域類轉換為軟件類。為了簡單處理,接下來的分析中,輸入輸出設備不做轉換。
經過篩選後,剩下的領域類就需要都轉換為軟件類,具體如下:
收銀員、商品、交易、小票、支付、信用卡、會員卡、現金、購物卡。
【名稱映射】
篩選完成後,我們開始講領域類轉換為軟件類,轉換的方法很簡單,首先不管三七二十一,將每個領域類都用一個軟件類與對應,名稱都保持一樣即可。
有的同學可能擔心這樣設計是否會不符合麵向對象設計的要求,是否會導致設計質量不高。。。。。。等等,其實這種擔心是多餘的,因為我們後續還有很多工作要做,目前做的隻是一個開始工作。
【屬性映射】
通過名稱映射的方法得到軟件類後,接下來就是要設計類的屬性了。由於領域類中也已經有了屬性,因此我們也隻需簡單的照搬過來即可。
【提煉方法】
軟件類的屬性設計完成後,接下來就需要設計軟件類的方法了。但這次我們就沒有那麼好的運氣了,因為領域類中並沒有方法!因此我們不能通過簡單映射的方法來獲取方法,必須采取其它手段。
和類的設計一樣,類方法的設計同樣不能采取“創造力、參考其它係統、拍腦袋”等方式來完成,為了確保正確性,類的方法設計也同樣應該能夠從已有的模型中推導出來。
由於已經明確領域模型中沒有方法了,因此就不能從領域模型中得到軟件類的方法,剩餘隻有一個“用例模型”了,因此我們鎖定“用例模型”,看看如何從中找到我們所需要的方法。
其實方法也很簡單,概括一下就是:找動詞。
你可能不敢相信自己的眼睛,這麼簡單,那幾乎初中生都會做設計啊,找動詞誰不會呢?
然而不管你信不信,這一步確實是這麼簡單,當然,如果麵向對象設計隻是到此為止,那確實初中生也是可以做的,但實際上這隻是麵向對象類設計的開始步驟而已,後麵的工作還多著了,所以完全不用擔心初中生來搶你的飯碗。
我們以POS機為例,來看看如何通過“找動詞”這種技巧來找到軟件類的方法。
如下是POS機的用例,我們將相關動詞都加粗顯示:
【用例名稱】 買單 【場景】 Who:顧客、收銀員 Where:商店的收銀台 When:營業時間 【用例描述】 1. 顧客攜帶選擇好的商品到收銀台; (這一步沒有異常) 2. 收銀員逐一掃描商品條形碼,係統根據條形碼查詢商品信息; 2.1 掃描儀壞了,必須支持手工輸入條形碼; 2.2 商品的條形碼無法掃描,必須支持手工輸入條形碼; 2.3 條形碼能夠掃描,但查詢不到信息,需要收銀員和顧客溝通,放棄購買此產品 3. 掃描完畢,係統顯示商品總額,收銀員告訴顧客商品總額; (這一步沒有異常) 4. 顧客將錢交給收銀員; 4.1 顧客的錢不夠,顧客和收銀員溝通,刪除某商品; 4.2 顧客的錢不夠,顧客和收銀員溝通,刪除某類商品中的一個或幾個(例如買了5包煙,去掉兩包) 4.3 顧客覺得某個商品價格太高,要求刪除某商品; 4-A:顧客使用信用卡支付 4-A.1 信用卡支付流程(請讀者自行思考完善,可以寫在這裏,如果太多,也可以另外寫一個子用例) 4-B:顧客使用購物卡支付 4-B.1 購物卡支付流程 4-C:顧客使用會員卡積分支付 4-C.1 會員卡積分支付流程 5. 收銀員清點錢數,輸入收到的款額,係統給出找零的數目; (這一步沒有異常) 6. 收銀員將找零的錢還給顧客,並打印小票; 7. 買單完成,顧客攜帶商品和小票離開; 【用例價值】 顧客買完單以後,就可以攜帶商品離開,而超市也將得到收入; 【約束和限製】 1. POS機必須符合國標XXX; 2. 鍵盤使用中文,因為收銀員都是中國人; 3. 一次買單數額不能超過99999RMB; 4. POS機要非常穩定,至少一天內不要出現故障; |
標識出所有的動詞後,還需要進一步的工作:
【篩選】
並不是所有的動詞都一定是軟件類的方法,我們需要將這些動詞識別出來並排除在後續設計範圍之外。
例如:
“顧客攜帶選擇好的商品到收銀台”:這裏的“攜帶”是顧客的一個動作,而顧客並不是我們的軟件類;
“收銀員告訴顧客商品總額”:這裏的“告訴”確實是收銀員的一個動作,而且“收銀員”確實也是我們的軟件類,但這裏也要排除“告訴”,因為“告訴”這個動作和POS係統並沒有關係,隻是業務流程中的一個步驟而已。
其它需要排除的動詞還有:“需要收銀員和顧客溝通”、“顧客將錢交給收銀員”、“收銀員清點錢數”、“收銀員將找零的錢還給顧客”、“顧客攜帶商品和小票離開”
【提煉】
篩選完不需要的動詞後,剩下的就是我們需要的動詞了,但此時並不能簡單的將所有動詞拿出來直接扔給某個軟件類就行了,我們還需要進行一些加工。
繼續以POS機為例:
“收銀員逐一掃描商品條形碼”:這裏的“掃描”看起來是“收銀員”的一個動作,而且“收銀員”確實也是我們的軟件類,但其實深究一下,“掃描”這個動詞並不能分配給“收銀員”這個軟件類,因為真正執行“掃描”功能的是“掃描儀”,收銀員隻是拿著掃描儀掃描商品,並不是收銀員自己去讀取商品條形碼;類似的動詞還有“必須支持手工輸入條形碼”,也不能算作“收銀員”的功能。
那我們為什麼不排除這兩個動詞呢?秘密就在於我們要從這兩個動詞提煉出軟件類的方法。稍作分析,我們就可以發現,無論是“掃描條形碼”,還是“手工輸入條形碼”,其實最終的目的都是“添加本次交易的商品”,因此我們可以提煉出“增加交易商品”的動詞。
還有一種提煉的方法需要從已有的動詞中推斷出來,例如:“掃描完畢,係統顯示商品總額”,這裏隻提到了“顯示”這個動詞,但相信大部分人都能一眼看出,“顯示”之前肯定要“計算”,不然顯示出來的值從哪裏來呢?
有的朋友可能會疑惑,為什麼不在用例的時候就寫清楚呢?例如:掃描完畢,係統計算商品總額,然後係統顯示商品總額。這樣不就一目了然的看出來了麼?
理想情況下這種想法當然沒錯,但現實往往沒有那麼美好,寫用例的產品人員可能經驗不足,也可能表達能力有限,還有可能比較馬虎,或者遺漏了。。。。。。總之會有很多異常情況,因此設計人員必須具備這樣的推斷和判斷能力。
經過這一步驟後,我們獲得的動詞如下:
* 增加商品
* 計算商品總額
* 顯示商品總額
* 刪除商品
* 現金支付
* 信用卡支付
* 購物卡支付
* 會員卡積分支付
* 打印小票
當然,以上列出來的動詞並不是就一定是100%的標準答案,不同的人來進行分析和設計,可能略有不同,但總體應該比較相似,畢竟業務是一樣的,而業務需求就是設計最強的約束。
【分配】
識別出有效的動詞後,最有一步就是分配了,即:將從用例中提煉出來的動詞,分配給已經有了屬性的軟件類。這種分配操作很多時候都是按圖索驥,特別是對於有領域經驗的人來說,基本上憑直覺就能基本分配正確。
當然,如果你的經驗並不是很豐富,那麼還是老老實實的一個一個來分析吧。
以POS機為例:
* 增加商品:很明顯應該分配給“交易”類
* 計算商品總額:分配給“交易”類
* 顯示商品總額:分配給“交易”類
* 刪除商品:分配給“交易”類
* 現金支付:分配給“現金”類
* 信用卡支付:分配給“信用卡”類
* 購物卡支付:分配給“購物卡”類
* 會員卡積分支付:分配給“會員卡”類
* 打印小票:這個動詞的分配存在一定的靈活性,有的人可能認為應該分配給“交易”類,因為打印小票可以認為是“交易”流程中的一個步驟;有的人可能認為應該分配給“小票”類,因為打印小票可以認為是“小票”類的一個基本功能。其實兩者都有一定道理,如果沒有其它更有力的選擇因素,我建議根據個人經驗選擇一個即可,這裏我們選擇分配給“小票”。
分配完成後,我們可以看到“交易”、“小票”、“信用卡”、“購物卡”、“會員卡”、“現金”都已經有方法了。
當然,對於有經驗的人來說,以上步驟完全可以在腦海中就迅速完成了,而並不會這樣一步一步的演示給別人看,所以看起來就像變戲法一樣,不知怎麼就設計出來了很多的軟件類。
經過上麵的處理步驟後,我們得到如下的類圖:

與領域模型相比,部分領域類被剔除了,留下來的領域類映射成軟件類後,又增加了方法。雖然還不完善,但軟件類的是越來越有型,越來越清晰了。
第二斧(精雕細琢):應用設計原則和設計模式
完成了從領域類到軟件類的映射後,類出來了,屬性也出來了,方法也有了,看起來設計已經大功告成了。
事實上也確實有很多人基本上做到這一步就開始動手編碼了,而且經過一番拚搏,最後發布的係統也能用。
但相信很多人都會有這個疑問:這樣做就夠了麼,這樣設計是否是好的設計呢?
要回答這個問題,我們首先要明確:什麼叫做“好”的設計呢 ?
到目前為止,我們已經有了一個類的設計模型,而且如果按照這個模型去實現的話,最終應該也是能夠滿足用戶的需求,畢竟我們這個類模型是按照“需求模型 -> 領域模型 -> 類模型”這樣一路推導過來的,不會出現大的偏差。
那麼,滿足了用戶需求的設計就是好的設計麼?
相信有經驗的朋友都會知道答案:“滿足用戶需求”隻是設計的一個最基本要求,而不是一個“好設計”的評判標準。
既然如此,那麼到底什麼才是好的設計呢,是否有明確的標準來進行評價呢?
幸運的是,麵向對象領域經過幾十年的發展,確實已經發展出了很多成熟的指導思想和方法,用於評價和指導如何才能做好麵向對象的設計。其中最具代表性的就是“設計原則”和“設計模式”。
【設計原則】
當我們談到麵向對象領域的設計原則的時候,我們其實都是在談論羅伯特.C.馬丁(Robert C. Martin ,又叫Bob大叔)的SOLID原則。
這也難怪,Bod大叔實在是太牛了,麵向對象領域的設計原則幾乎被他全部包攬了,加上他在他的暢銷書《敏捷軟件開發:原則、模式與實踐》中詳細的將這些原則集中一 一闡述,麵向對象領域設計原則的權威非他莫屬。毫不誇張的說,Bob大叔的威名和在麵向對象領域中的地位,和設計模式的“四人幫”是不相上下的。
雖然很多資料都將SOLID原則和敏捷開發、測試驅動開發等方法綁定在一起,但我覺得隻要是麵向對象設計,不管是瀑布流程、、CMM流程、RUP流程、還是敏捷開發流程,都應該應用設計原則以提高設計質量。
參考wiki百科,SOLID設計原則簡單介紹如下:
SOLID實際上是取5個設計原則的首字母拚起來的一個助記單詞。具體的設計原則如下(詳細的設計原則,我們會在後麵詳細闡述,這裏不再詳細展開):
首字母 |
英文簡寫 |
英文名稱 |
中文名稱 |
說明 |
S |
SRP |
Single Responsibility Principle |
單一職責原則 |
對象應該隻具備單一職責 |
O |
OCP |
Open/Close Principle |
開放/封閉原則 |
認為“軟件體應該是對於擴展開放的,但是對於修改封閉的”的概念。 |
L |
LSP |
Liskov Substitution Principle |
Liskov替換原則 |
認為“程序中的對象應該是可以在不改變程序正確性的前提下被它的子類所替換的”的概念 |
I |
ISP |
Interface Segregation Principle |
多個特定客戶端接口要好於一個寬泛用途的接口 |
|
D |
DIP |
Dependency Inversion Principle |
依賴於抽象而不是一個實例 |
(wiki百科鏈接如下:
https://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1))
前麵我們簡單的八卦了一下,現在回歸正題:設計原則有什麼用?
其實和所有的原則一樣,設計原則也是一個判斷標準,說通俗點,設計原則就像是木匠手中的尺子,尺子是用來衡量木材的長短的,而設計原則就是衡量類設計的“尺子”:量一量,看長了還是短了,還是正好,長了就裁短一些,短了就加長一些。經過如此衡量並調整,最終就能夠得到我們希望的設計作品。
當然,和木匠的尺子稍有不同,木匠不用尺子就做不出能用的家具,但我們不用設計原則的話,其實還是能夠做出滿足需求的係統的。
既然這樣,我們為什麼一定要用設計原則呢?ARTHUR J.RIEL在《OOD啟思錄》一書中針對這個問題給出了非常形象的解釋:
你不必嚴格遵守這些原則,違背它也不會被處以宗教刑罰。但你應當把這些原則看做警鈴,若違背了其中的一條,那麼警鈴就會響起。”-------ARTHUR J.RIEL,《OOD啟思錄》
也就是說,如果違背了這些設計原則,就可能有危險,但究竟是什麼危險呢,警鈴要警告我們什麼呢,是火災、水災、地震、陷阱、還是有獅子、老虎。。。。。?
要回答這個問題,還需要回到麵向對象的本源:我們在第一章解釋為什麼要麵向對象的時候提到了麵向對象的核心思想是“可擴展性”,這其實就是我們應用設計原則的根本目的:保證可擴展性。如果我們不遵守這些設計原則,警鈴就會響起,提醒我們:你的設計可擴展性會有問題!
除了設計原則外,後麵要講到的設計模式,其本質也是為了提高可擴展性。這也是為什麼我們通過領域類映射得到了很多軟件類之後,還需要不辭辛勞的繼續應用設計原則和設計模式的主要原因,本質上都是為了提高設計的可擴展性。
SOLID設計原則的各個子原則詳細介紹會在後麵詳細介紹,這裏我們簡單的以POS機為例,看看如何應用設計原則。
仔細觀察我們通過領域類映射得到的軟件類,可以發現一個很明顯不符合SOLID原則中的DIP原則的地方,即:“交易類”直接依賴“會員卡”、“購物卡”、“信用卡”、“現金”4個子類,這樣的實現不符合DIP原則,當需要增加新的支付方式時,“交易類”也需要跟著修改。
既然不滿足DIP設計原則,那麼我們就按照DIP原則的要求,提取出一個支付的父類來,即:“交易類”依賴“支付類”,“會員卡”、“購物卡”、“信用卡”、“現金”都繼承“支付”類。具體實現如下:
對於其它各個類,我們都可以依次使用設計原則進行判斷,當發現不符合設計原則的設計時,就采取增加、刪除、合並、拆分等手段,使我們的設計逐步改進,最終達到符合設計原則的目的。
【設計模式】
相比設計原則來說,設計模式更加普及和流行,當我們談到設計方法的時候,大部分人肯定都會想到設計模式,設計模式如此深入人心,幾乎到了言必談設計模式的地步。
和設計原則類似,當我們談論設計模式的時候,我們其實都是在談論GOF(Gang of Four,中文翻譯為“四人幫”)在經典名作《設計模式 --可複用麵向對象軟件的基礎》一書中提到的設計模式。
通俗的講,設計模式是用於指導我們如何做出更好的設計方案,而前麵提到的設計原則,其作用也是這樣的。那麼,設計原則和設計模式,我們該如何選擇?
有的朋友可能會以為這兩個是二選一的關係,要麼用設計原則,要麼用設計模式。這種理解是錯誤的,設計原則和設計模式並不是競爭關係,正好相反,它們是互補的關係。
設計原則和設計模式互補體現在:設計原則主要用於指導“類的定義”的設計,而設計模式主要用於指導“類的行為”的設計,更通俗一點的講:設計原則是類的靜態設計原則,設計模式是類的動態設計原則。
一般情況下,我們是采用“先設計原則,後設計模式”的方法來操作的。
設計模式的相關內容會在後文詳細介紹,這裏我們以POS機為例,看看如何應用設計模式來優化我們的設計。
通過分析應用設計原則優化後的類,我們發現“信用卡”這個類存在優化的空間,因為國際上存在不同的信用卡,最常見的有中國銀聯(UnionPay)、Visa、MasterCard這幾種,每種信用卡在支付的時候需要接入不同的機構,其接入方式和協議肯定都是有一定差異的。為了封裝這種差異以支持後續更好的擴展,我們應用設計模式的Bridge模式,提取出“信用卡處理”這個類,這個類的主要處理“連接、認證、扣款”這樣的職責。UnionPay、Visa、MasterCard都繼承“信用卡處理”這個類。具體如下:

第三斧(照本宣科):拆分輔助類
經過前麵的設計步驟之後,麵向對象類的設計工作已經完成,我們輸出了完整的類模型,看起來已經可以開始動手編碼了,你是否舒了一口氣,看著自己的設計作品,不由得產生了一種自豪感呢?
確實值得自豪,畢竟我們一步一個腳印,從最初僅僅存在於客戶腦袋中的需求,逐步的推導、演變、設計出了能夠付諸實施的類模型了。但在最終實施之前,還有一點小小的動作要完成,這就是我們的拆分輔助類操作。
拆分輔助類的主要目的是為了使我們的類在編碼的時候能夠滿足一些框架或者規範的要求。比如說常見的MVC模式,將一個業務拆分成Control、Model、View三個元素;J2EE模式中,將對象分為PO、BO、VO、DTO等眾多對象。
之所以說這是一點小小的動作,是因為這個動作確實很簡單,隻要將我們設計出來的類,按照規範要求,一 一對應分拆即可。
以POS機為例,假如我們的框架要求提供DAO對象,負責數據庫的相關操作,則“購物卡”類就應該拆分為兩個:“購物卡”、“購物卡DAO”,其中“購物卡”用於負責提供“支付”功能給“交易”類調用,“購物卡DAO”用於負責從數據庫讀取購物卡信息,修改數據庫中購物卡餘額等操作。

需要注意的是,拆分設計輔助類僅僅是為了滿足框架或者規範的要求,本身並不是一個設計的步驟,而是實施的一個步驟,所以我們一般都不需要將拆分的輔助類體現在類模型中,僅僅在編碼的時候拆分即可。
================================================
轉載請注明出處:https://blog.csdn.net/yunhua_lee/article/details/23738671
================================================
最後更新:2017-04-03 12:56:16