1. 程式人生 > >CodeArt入門教程(三)

CodeArt入門教程(三)

pan center account 根據 領域對象 保存 顏色 單一職責原則 用例

5.領域模型設計

  下面我們創建賬戶子系統(AccountSubsystem),賬戶子系統雖然被門戶服務使用,但是子系統本身是獨立於任何服務存在的。所以我們為賬戶子系統創建獨立的項目解決方案:

技術分享

  子系統的項目解決方案比服務的項目解決方案需要引用的程序集少很多。除了解決方案文件夾Framework裏需要引用幾個CodeArt提供的類庫外,僅需額外創建一個AccountSubsystem的類庫項目,稍後我們會在這個程序集裏創建領域模型。AccountSubsystemTest則是針對賬戶子系統的單元測試。

  大家應該還記得,我們在門戶服務的解決方案裏也添加了一個專門針對門戶服務的單元測試PortalServiceTest。嚴格的講,有效的自動化測試越多越好,這樣會給系統的維護帶來極大的好處。但是測試是有成本的,編寫測試用例、編碼實現測試用例都是需要時間完成的。所以在項目開發的過程中我們會更多的關註服務的測試,因為服務的測試會覆蓋到子系統的代碼,測試服務也會間接測試到子系統,一舉多得節約成本。只有當整個項目快結束的時候或者有空閑時間了,我們才會再來補充子系統的測試。

  所以,當我們創建了AccountSubsystem項目解決方案並將代碼提交到代碼庫後,可以直接關閉該解決方案。打開PortalService,將AccountSubsystem子系統引用到PortalService的Subsystems解決方案文件夾裏:

技術分享

  至此,為PortalService創建賬戶子系統的工作就完成了。我們可以開始考慮如何構建賬戶子系統的領域模型了。在討論這一話題之前,我們先來看看關於CA裏領域模型的基本概念。

  概念1:領域對象,領域模型裏的一切對象都應該是領域對象。所謂的領域對象就是指遵守領域規則的對象,這是它與普通對象最大的區別。領域規則主要由以下幾點組成:

  1) 領域對象永遠都不會為null。在詳細說明這一點之前,大家一定要知道這個規則跟數據庫裏的字段不能為null沒有任何關系。到目前為止,我們都沒有提到如何使用數據庫技術,這個教程之後的內容也不會講述與數據庫有關的話題,因為這些都與領域模型的建立沒有一丁點的關系,沒有數據庫一樣可以成功建立領域模型。各位一定要摒棄以前根深蒂固的開發思路,放空自己的思想,重新接收領域模型裏的概念。

  言歸正傳,在領域的世界裏,一切對象都是有其存在的意義的。什麽叫存在的意義?這體現在職責上,一個對象可以履行某項職責那麽這個對象就是有意義的存在。在領域世界裏絕對不會出現不具備任何職責的對象。大多數情況下,對象需要提供方法以便其履行職責(.Net裏的屬性也是方法,只是被包裝了而已)。

  每年企業都會向政府交納一定的稅收,這是企業的職責之一,所以領域世界裏的企業模型會有一個計算納稅金額的方法,該方法會根據企業的盈利計算出交稅的金額。如果我問你,你創辦的公司今年要交多少稅?你可以通過該方法計算結果,告訴我需要交納10萬元。那麽,你沒有公司呢?你根本就沒有自己創辦的公司,那你要如何回答我這個問題?回答“很抱歉,我沒有公司”嗎?錯,程序的世界是理性的世界,一切都是嚴謹的,計算納稅金額的方法定義返回值是浮點數,那麽這個方法在任何情況下都應該返回一個浮點數(拋出異常例外)。當我問你的公司要交納多少稅,你只能告訴我具體的數值,我不管你是否有自己的公司,那是你自己要考慮的事情。所以,你應該回答“0”。

  分析到這裏,大家發現一個問題沒有,一個沒有創辦公司的人卻也可以知道“自己創辦的公司”今年要交納多少稅收,只不過由於他實際上沒有公司,所以計算的結果為0。因此,判定事物能否履行某項職責和這個事物的實例是否存在沒有必然的聯系。我們只要設計了名稱為Corp的企業領域模型,在Corp類裏定義了方法CalculateTax(計算稅收),那麽在任何情況下,Corp的實例都應該可以成功的調用該方法,至於計算的結果由Corp內部去處理,外界不用考慮這些細節。所以,即使不存在編號為135的Corp對象,我們依然可以根據編號135找到一個為Empty的Corp對象,調用該對象的CalculateTax方法會計算並得到稅收值為0的結果。

  所以,在領域的世界裏,我們規定對象可以為Empty但是永遠不能為null,因為null表示不存在,不存在的對象無法履行任何職責。我們衡量對象是否存在的唯一依據就是職責,一個沒有職責的對象根本就不會被設計出來,所以領域世界裏沒有不存在的對象,而Empty則表示對象是存在的,只不過數據都是空的,空對象依然可以履行職責,只是履行職責的結果會和非空對象執行的結果有所不同。下圖是體現這一規則的INotNullObject接口定義:

技術分享

  2) 每個領域對象都具有驗證固定規則的能力。固定規則與業務規則不同,這組規則不會隨著使用對象的場景的變化而變化,它是對象自身固有的規則。例如人的年齡不可能有上萬歲,汽車的輪胎個數也不會有上百個,這些都是領域模型裏的固定規則,不會隨著人或汽車這一事物在不同的使用場景裏而發生規則的改變。CA通過實現ISupportFixedRules接口來完成領域對象驗證固定規則的能力:

技術分享

  3) 我們可以明確的知道領域對象的倉儲狀態,領域對象的倉儲狀態與它在倉儲中保存的數據映射有關。當新建一個領域對象時,該對象的倉儲狀態就是“新建”的;使用完領域對象,將其存入倉儲後,該對象的狀態就是“幹凈”的;我們從倉儲中獲取一個對象,並更改了對象的屬性值,但還未提交給倉儲再次保存的時候,該對象的狀態就是“臟”的。對象的倉儲狀態與對象的持久化操作息息相關,所以CA明確規定每個領域對象都能提供和更改自身的倉儲狀態,該領域規則由IStateObject詮釋:

技術分享

  除了以上3項規則外,我們在設計領域對象時還需要遵循一些其他的領域規則,這些規則會以約定的形式給出而不是顯示的接口,這在後面的實踐中會詳細討論。所有的領域對象都實現了IDomainObject接口:

技術分享

  概念2:實體對象。這類對象具有可區別性,可以與其他事物區分開來。不同顏色、款式的衣服肯定是不同的實體,但是同樣顏色、同樣款式的衣服就一定是同一個實體嗎?在現實世界,人們可以感性的判定事物的唯一性,比如我買的衣服和你穿的衣服就算一模一樣,但是它們理所當然的不是同一件衣服。然而在程序世界一切都是理性的,所有判斷都是要有根據的。

  領域實體對象的職責之一就是幫助程序辨別不同的對象實例,區分事物的唯一性。每個實體對象都需要提供唯一的標識符來標識自己的存在。我們能夠通過該標識符找到唯一一個對應的實體,這是實體對象的重要特征。通過標識符可以引用到一個對象,這也是實體對象常被稱為引用對象的原因。

  我們可以為衣服設置一個叫做編號的唯一標示符。我買的衣服編號為1,你穿的衣服編號為2,就算衣服的其他屬性值都相同,但是由於唯一標識符不同,所以這兩件衣服在系統看來是不同的實體。當我調用洗衣機的方法去洗我的衣服(編號1),不會對你的衣服(編號為2)造成影響。也就是說,隨著時間軸的推移,實體對象的狀態會由於各種領域行為的發生而導致了改變,但我們依然可以根據標識符找到目標實體並關註狀態變化的情況。以洗衣為例,我的衣服(編號1)變的幹凈了,因此我很滿意。而你的衣服(編號2)依然那麽臟,但與我無關。

  這正是我們使用實體對象的根本原因:追蹤目標對象狀態的變化情況,以便其行為更清楚且可預測。也就是說,當我們需要持續關註一個事物的變化情況時,我們應該將該事物的模型設計為實體對象。以下是CA裏實體對象必須實現的接口(該接口不必程序員實現,CA提供了實體對象的基類):

技術分享

  概念3:值對象。如果一個對象的所有屬性都是用於從某種角度來描述另外一個事物的狀態時,這個對象就是值對象。

  繼續以衣服為例,我們可以把衣服的顏色、圖案、尺碼這3個屬性提取出來,作為一個單獨的值對象叫“外貌”,再由衣服去引用這個“外貌”。這樣我與你的衣服雖然不是同一件衣服,但是外貌特征可以相同,都是白色、花型圖案、XL碼的體恤衫。由於衣服是實體對象,我們依然可以清楚的區分衣服的唯一性,但是我們的衣服共用了相同的外貌特征。所以,值對象是沒有唯一性判斷依據的,它只是多個描述事物狀態值的綜合載體,如果兩個值對象的屬性值都相同,那麽我們認為這兩個值對象是同一個對象,因為他們描述事物的結果是一樣的。使用值對象需要遵守兩個領域規則:

  1) 值對象的所有屬性都是只讀的,只有當構造它的時候才能傳入值,構造完畢後,值對象的屬性將無法更改。這意味著當我想把衣服染成紅色的時候,我只能新建一個“紅色、花型圖案、XL碼”的外貌特征,並將衣服的外貌屬性設置成新建的這個值對象。我不能更改現有“外貌”的“顏色”屬性,因為一旦更改了,你衣服的外貌也被更改了,因為我和你的衣服都是引用的同一個外貌特征值對象。因此,在CA裏,值對象的設計必須保證屬性是只讀的。

  2) 我們要保證值對象裏的所有屬性都是從同一個角度去描述事物的。使用值對象的一個很重要的原因就是將復雜事物的屬性提取出來,單獨作為一個對象管理,這樣可以提高程序的可維護性。例如在訂單這個事物裏涉及到的信息會很多:購買的商品、商品數量、優惠活動、購買人、支付方式、發貨方式等。其中“收件人所在的省份城市”、“詳細地址”、“郵編”這三項信息都與收貨地址有關,我們可以把這類描述收貨地址的屬性集中起來,設計一個地址(Address)的值對象來管理他們。訂單就可以使用該對象來承擔與收貨地址相關的職責。因此,如果一個值對象的屬性是雜亂無章的,不能從同一個角度描述事物的特點,那這個值對象是沒有存在價值的。

  概念4:內聚模型。一個模塊內部各個元素彼此結合的越緊密則它的內聚性越強。也正是由於這些元素結合的非常緊密,他們往往也只負責某一項任務,這也就是所謂的單一職責原則。

  我們之前創建了門戶服務,因為我們認為菜單、角色、權限等事物是整個項目運行的基礎,這幾個事物之間或多或少存在某些聯系,有一定的內聚性,所以將這幾個事物劃分到一個服務裏共同驅動門戶服務工作。

  另外,角色、權限、賬號三者的關系遠比菜單更加緊密,即使沒有菜單對象,他們仍然可以共同擔負起身份識別的職責。因此我們將他們納入到賬戶子系統中,賬戶子系統可以脫離門戶服務獨立被其他服務使用。所以,賬戶子系統是一個比門戶服務更高的內聚模塊。子系統的內聚性要遠高於服務。

  那麽在子系統內部呢?子系統內部我們依然可以按照類似的思路,根據對象之間的緊密程度劃分出更高的內聚模型,這就是我們要熟悉的第4個概念:領域模型裏的內聚模型。在詳細討論這個概念之前,我們需要搞清楚如何判斷對象之間是緊密的。如果對象A引用了對象B,當A在履行某些職責的時候,需要對象B的支持,我們就說A依賴於B。這在程序實現裏常常表現為當調用A的方法MA時,MA內部又調用了B的方法MB來協助MA順利的執行,這種情況下A和B的關系就比較緊密。那麽,有沒有比這種AB關系更加緊密的關系呢?有的。如果對象A的生命周期依賴於對象B的生命周期,也就是說,只有B出現在了程序裏,A才有可能出現,而當B被銷毀了,A就一定會被銷毀。對象A的生命周期始終依賴於對象B的生命周期,那麽這種關系就是更加緊密的,這也就是內聚模型裏的對象之間的關系。使用內聚模型的領域規則如下:

  1) 每個內聚模型裏都會有且僅有一個內聚根,內聚根是一個實體對象。這意味著你可以通過唯一標識找到該內聚根。

本章節未完,稍後更新全部內容。。。。。。

CodeArt入門教程(三)