關於借書場景的領域建模,我從以下幾個方面進行分析:
分析模型靜態結構
我分析一個領域模型的靜態結構的思路一般是:先找出我們需要關心的對象,對於借書這個場景,我們關心的有:
1. Account(賬號):Id(賬號唯一標識,自動生成), Number(卡號), Owner(賬號當前擁有者用戶信息), BorrowedBooks(賬號當前借到的書)
2. Book(書本):Id(唯一標識,自動生成),BookInfo(值對象,包含書本基本信息),Count(表示當前庫存數量)
3. BorrowHistory(借書歷史、借書日志):AccountId(借書賬號),BookId(書本Id),Count(數量,表示借了幾本),BorrowTime(借書時間)
4. BorrowedBook(借到的書):BookId(書本Id),Count(書本數量)
通過上面的分析,那么模型的靜態結構就很容易畫出來了。
按面向過程的思維實現邏輯
這種分析思路是最容易的,因為我們不用考慮對象之間如何交互,我們只需要考慮場景結束后,每個對象會發生什么變化即可。所以,按照這個思路,我們得出借書這個場景發生后有以下幾個對象會發生變化:
- Book的Count屬性會變化(減少,因為書本被借出);
- Account的BorrowedBooks屬性會增多(因為借到書);
- BorrowHistory會被創建,因為發生了一次借書的操作;
上面這3步在經典DDD中,我們通常會設計一個領域服務來完成,比如叫BorrowBookService
可以發現,其實面向過程的分析思路是一種面向結果的分析方法;我們只需要考慮一個交互過程的結果改變了哪些對象的什么狀態即可,而對象之間到底如何交互的我們不用顯式的建模出來;所以這種建模方法相對簡單,因為我們考慮的東西比下面第這種方式要少一樣東西,那就是對象之間的交互。
按面向對象的思維實現邏輯
也許有人會說,上面的面向過程的建模思路不是真正的OO,因為對象之間沒有交互,對象只是一個data,只是一個對數據的封裝而已。
那么,如果要讓對象之間體現出交互,那我們該如何分析呢?我覺得最關鍵的是要把握一點:我們分析的時候要時刻按照“誰通知誰做什么事情,或誰被通知做什么事情”這個思路來分析;
好,那按照這個思路,那么對上面的對象:Account,Book,BorrowedHistory,我們如何來分析呢?
首先假設我們已經設計好了這個軟件,然后有一個界面顯示在屏幕上,然后用戶用它的卡號(account.Number)登陸了系統,然后用戶通過查詢選擇了幾本書,然后點擊“借書”按鈕。整個借書場景就是從這個“借書”按鈕開始啟動。另外,整個借書場景的參與者信息有:accountId,bookId,count,表示哪個賬號對哪本書借了幾本。
好,那么,“借書”按鈕被點擊后,應該有一個對象被激活,先不管該對象是什么,我們只要知道該對象會做一件事情,就是:
var borrower = repository.load<Account>(accountId); //將借書人賬號load到內存 var book = repository.load<Book>(bookId); //將書本對象load到內存 borrower.BorrowBook(book, count); //啟動賬號的借書行為
接下來borrower.BorrowBook方法內部會發生什么呢?想想現實世界怎么發生的就知道了,你去圖書館借書,你肯定告訴管理員說“我要借這幾本書”,這句話潛在的意思是,請你把這幾本書借給我,謝謝,呵呵。
那就很明白了,應該有一個對象,如圖書館管理員(administrator),他有借出書(LendBook)的職責行為,那么上面的BorrowBook方法內看起來就是如下這樣:
borrower.BorrowBook(book, count) { //通知圖書館管理員把指定的書借給我count本,this就是我,呵呵 administrator.LendBook(this, book, count); //管理員把書借出來后,更新賬號自己的當前借到的書的信息 var borrowedBook = _borrowedBooks.SingleOrDefault(x => x.BookId == book.Id); if (borrowedBook == null) { borrowedBooks.Add(new BorrowedBook(book, count)); } else { borrowedBook.AddBookCount(count); } }
接下來我們可以考慮administrator.LendBook這個行為做了什么?
administrator.LendBook(account, book, count) { //通知書本減少其庫存數量 book.DecreaseCount(count); //方法內部需要檢查庫存數量是否足夠,如果不夠需要拋異常; //這里應該要記錄借書記錄了,因為譯本書是否被借出的衡量標准是圖書館管理員說了算的,當他 //用掃描儀對該本書進行了掃描並確認后,就表示該本書確定被借出去了,所以我們可以在這里做 //創建借書記錄的邏輯。 var borrowHistory = new BorrowHistory(account, book, count, DateTime.Now); //下面理想情況下我們不希望在這里保存borrowHistory,但是如果光是new一個BorrowHistory對象出來, //是沒辦法被持久化出來的,必須通過某種方式通知框架保存new出來的這個對象, //如果用經典的ddd,那如何保存borrowHistory呢?后面我會談到一些關於這個的思考。 }
好,上面的分析我想應該很清晰地表達了對象之間如何交互,從而完成整個借書場景。但是,上面提到“借書”按鈕被點擊后,應該有一個對象被激活。那么這個對象會是什么呢?
我覺得你可以設計一個BorrowBookService領域服務,也可以設計一個BorrowBookContext場景類,它有一個Interaction(交互的意思)方法:
borrowBookContext.Interaction(Guid accountId, Guid bookId, int count) { var borrower = repository.load<Account>(accountId); //將借書人賬號load到內存 var book = repository.load<Book>(bookId); //將書本對象load到內存 borrower.BorrowBook(book, count); //啟動賬號的借書行為 }
所以,整個交互的過程就是:
- 軟件使用者(user)通知系統(system)我要借書;
- 系統於是創建一個BorrowBookContext場景對象,並通知該場景對象啟動交互過程(Interaction);
- borrowBookContext通知倉儲(repository)將account,book這兩個對象從內存激活,通過load方法實現;
- 然后通知借書賬號執行其借書行為,其實此時借書賬號是扮演了IBorrower角色(即借書人的角色),所以嚴格來講,BorrowBook這個行為是屬於IBorrower這個角色的;
- 然后borrower通知administrator把書借出來;
- 然后administrator通知book減少其余額;
- 然后administrator創建借書記錄,即產生借書日志;
從上可以明顯的看出,每一個交互都是對象a通知對象b做什么,這是關鍵;上面的分析和代碼充分體現了“對象交互”,也許這樣的代碼才是更OO吧,呵呵。
為了大家更好的理解這種OO的方式,我特地寫了一個完整的例子。源代碼下載地址:http://files.cnblogs.com/netfocus/BookLibraryExample.rar
最后一些補充
- 因為,現實生活中,你去圖書館借書,那執行借出書的那個管理員(administrator)代表的就是圖書館。甚至我們可以這樣想,假設現在有一個自動借書機或借書網站,你插入你的借書卡(網站用戶登錄),然后輸入要借的書,然后點擊確定,然后書本就自動從借書機里吐出來了,呵呵。如果是網站,那就是會自動郵寄過來(當然還要輸入寄送地址,呵呵)。所以,從這個分析可以知道,其實圖書館管理員不重要,它其實代表的是圖書館,而圖書館本質上就是提供借書服務。當然,因為我們上面只考慮的借書的場景,我們有沒有想過books這個集合放在哪個對象上比較合適呢?我覺得很顯而易見把,那就是圖書館,即library.Books,圖書館維護了所有的書本信息;所以,從整體來看,圖書館也有狀態。
- 有時我們認為產生借書日志不是核心領域邏輯,因為並不是所有的圖書借閱系統都需要記錄借書記錄,那這樣的話,我們可以在應用層(也就是我上面的BorrowBookContext中)生成借書記錄;
- 雖然按照OO的思路去領域建模出來的結果看起來很舒服,但實際上不是很實用,我個人認為屬於中看不中用的設計,呵呵。因為這樣的設計雖然做到了對象與對象之間的交互,但實際上當我們在面對並發和數據一致性時,都會引入事務。像上面的分析,我們知道一次借書,至少會影響3個聚合根的修改或新增,那意味着一次事務會跨3個聚合根。一旦引入事務,那在當用戶訪問量大,並發高的情況下,系統可用性是很差的;所以,國外DDD專家才推薦,一次事務只更新一個聚合根,那如果要遵守這樣的規定,那如何實現上面的一次要修改3個聚合根的需求呢?呵呵,為了解決這個問題,我們需要通過saga了,就是類似於一個流程管理器的東西。引入saga,相當於實現了聚合根與聚合根之間的異步通信,而不是直接調用聚合根的方法通知其做事情;上面的設計的最大問題就是都是某個聚合根直接調用另一個聚合根的方法通知其做事情。實際上每個聚合根都自己內部維護了其一致性,聚合根之間完全可以通過異步的方式實現交互。saga就是用來實現聚合根之間異步交互的一種技術。saga就是將方法調用修改為:publish-subscribe,以及command的模式,呵呵。學習Saga的一個例子可以看看這篇文章:http://msdn.microsoft.com/en-us/library/jj591569.aspx