好久沒有寫文章了,最近比較忙,另一方面也是感覺自己在這方面沒什麼實質性的突破。但是今天終于感覺自己小有所成,有些可以值得和大家分享的東西,并且完成了兩個可以表達自己想法的demo。是以,趁現在有點時間,是寫文章和大家分享的時候了。
首先給出這兩個demo的源代碼的壓縮包的下載下傳位址,因為之前有博友說他沒有裝vs2010而沒辦法運作demo,是以這次我分别用vs2008和vs2010實作了兩個版本。
<a href="http://files.cnblogs.com/netfocus/dcibasedddd.rar" target="_blank">http://files.cnblogs.com/netfocus/dcibasedddd.rar</a>
下面先分享一下我最近研究的一些知識及我對這些知識的自我感悟,然後再結合demo中的示例講解如何将這些感悟應用到實際。
我最近一直在學習下面這些東西:
面向對象分析與設計,即object oriented analysis and design(ooa\d)
領域驅動設計,即domain driven design(ddd)
四色原型:mi原型、role原型、ppt原型、description原型
dci架構:data context interaction
cqrs架構: 指令查詢職責分離原則,即command query responsibility segregation
通過學習以上這些知識,讓我對面向對象的分析、設計、實作有了一些新的認識。
2.業務模型建好了,該如何通過面向對象的分析與設計方法來進行對象模組化呢? ddd和dci思想可以幫助我們。首先,ddd能夠指導我們建立一個靜态的領域模型,該領域模型能夠清楚的告訴我們建立出來的對象“是什麼”,但是ddd卻不能很自然的解決“做什麼”的問題。大家都知道ddd在對象設計的部分實際上是一種充血模型的方式,它強調對象不僅有屬性還會有行為,如果行為是跨多個領域對象的,則在ddd中用領域服務解決。但是ddd卻沒有完整的考慮對象與對象之間的互動如何完成,雖然它通過領域服務的方式協調多個對象之間進行互動或者在應用層協調多個對象進行互動。但是在ddd中,對象往往會擁有很多不該擁有的屬性或行為。在我學習了dci架構之後,我認識到了ddd的很多不足。
以下是dci的核心思想:
對象扮演某個角色進入場景,然後在場景中進行互動,場景的參與者就是對象所扮演的角色;
一個對象可以扮演多個角色,一個角色也可以被多個對象扮演;
對象的屬性和行為分為:a:核心屬性和行為,這些屬性或行為是不依賴于任何場景的;b: 場景屬性和行為,對象通過扮演某個角色進入某個特定場景時擁有的屬性或行為,一旦對象離開了這個場景,不再扮演了這個角色後,這些場景屬性或行為也就不再屬于該對象了;比如人有核心的屬性和行為:身高、體重、吃飯、睡覺,然後當人扮演教師的角色在教室裡上課時,他則具有上課的行為,一旦回到家裡,就又變成了一個普通的人;比如一個物品,在生産時叫産品,在銷售時叫商品,壞了的時候叫廢品,它在不同階段扮演不同的角色所具有的屬性是不一樣的;
場景的生命周期,場景是一個時間與空間的結合,可以了解為某個活動;一旦活動結束,則場景也就消失;
dci中的d可以了解為ddd中的領域模型;場景中互動的是角色,而不是領域實體。場景屬于dsl的思考層面,更接近于需求和用例。而領域也是偉大的出現,但是不能為了領域而領域,為什麼呢?因為場景是大哥用例是大哥。領域的存在是為了控制固定概念的部分,這樣在某種成度上控制了一定的複雜性和提高了可控性,而dci則解決了可變性和需求的問題。從某種意義上來說,“領域層(在dci中可能不會太凸顯領域層,不如old ddd那麼凸顯)” 是為了dci架構服務的。
角色是人類的主觀意識,用于對象分析和設計階段,但是在運作階段,角色和對象實體是一體的,軟體運作過程中隻有對象,隻是這些對象在參與某個活動時扮演了某個角色而已;
3. 領域驅動設計中的對象設計部分的一些要點:
ddd的在對象設計方面的最大貢獻之處在于其實體、值對象,以及聚合邊界的三個部分,通過這三個概念,我們可以将對象的靜态結構設計好。
領域對象所包含的屬性必須是隻讀的,隻讀的含義是一旦對象被建立好,則隻有對象自己才能修改其屬性,屬性的類型可能是基本資料類型或值類型,即valueobject;
領域模型設計時不應考慮orm等技術性的東西,而應該隻專注于業務,不要讓你的領域模型依賴于技術性的東西;
領域對象的屬性和方法設計時要完全根據業務的含義和需要來進行,不要動不動就把每個屬性定義為get;set,這會導緻領域模型的不安全;
倉儲(repository)不是解決讓領域模型不依賴于外部資料存儲的唯一方式,我覺得還有更優雅的方式那就是事件驅動;
設計領域模型時不要考慮分層架構方面的東西,因為領域模型與分層架構無關;
不要認為領域模型可以做任何事情,比如查詢。領域模型隻能幫你處理業務邏輯,你不要用它來幫你做查詢的工作,那不是它擅長的領地,因為它的存在目的不是為了查詢;cqrs的思想就是指導我們:指令和查詢因該完全分離,領域模型适合處理指令的部分,而查詢可以用其他任何的不依賴于領域模型的技術來實作,甚至可以直接寫sql也可以;
分析領域模型及其對象之間的互動時,要厘清什麼是互動的參與者,什麼是互動的驅動者,通常情況下,比如人是互動的驅動者,而人在系統中注冊的某個帳号所扮演的角色就是互動的參與者;比如我用a的圖書卡去圖書館借書,則我是借書活動的驅動者,而a的圖書卡對應的帳号所扮演的借書者(borrower)角色就是借書活動的參與者;
前面的介紹看起來比較枯燥,但對我來說是非常寶貴的經驗積累。下面我通過一個例子分析如何運用這些知識:
以圖書管理系統中的借書和還書的場景進行說明:
1. 借書場景:某個人拿着某張借書卡去圖書館借書;
2. 還書場景:某個人拿着某張借書卡去圖書館還書;
根據四色原型的分析方法,我們可以得出:某個“人”以圖書借閱者的角色向圖書館借書。從這裡我們可以得出三個角色:1)借閱者(borrower);2)被借的圖書(borrowedbook);3)圖書館。那麼這三個角色的扮演者對象是誰呢?其實這是問題的關鍵!
1)是誰扮演了借閱者這個角色?很多人認為是走進圖書館的那個人,其實不是。 人所持的圖書卡對應的那個人才是真正的借閱者角色的扮演者;試想張三用李四的圖書卡借書,借書的是誰?應該是李四,此時相當于李四被張三操控了而已;當然這裡假設圖書館不會對持卡人和卡的真正擁有者進行身份核對。是以,借閱者角色的扮演者應該是借書卡對應的帳号(借書卡帳号本質上是某個人在圖書館裡系統中的鏡像)。那麼圖書卡帳号和借閱者角色有什麼差別?圖書卡帳号是一個普通的領域對象,隻包含一些核心的基本的屬性,如accountnumber,owner等;但是borrower角色則具有借書還書的行為;
2)是誰扮演了被借的書這個角色?這個問題比較好了解,肯定是圖書了。那圖書和被借的圖書有什麼差別嗎?大家都知道圖書是指還沒被借走的還是放在書架上的書本,而被借的書則包含了更多的含義,比如被誰借的,什麼時候借的,等等;
3)為什麼圖書館也是一個角色?圖書館隻是一個地點,它不管有沒有參與到借書場景中,都叫圖書館,并且它的屬性也不會因為參與到場景中而改變。沒錯!但是他确實是一個角色,隻不過它比較特殊,因為在參與到借書場景時它是“本色演出”,即它本身就是一個角色;舉兩個其他的例子你可能就好了解一點了:比如教室,上課時是課堂,考試時是考場;比如土地,建造房子時是工地,種植糧食時是田地,是有可能增加依賴場景的行為和屬性的。
有了場景和角色的之後,我們就可以寫出角色在場景中互動的代碼了。我們此時完全不用去考慮對象如何設計,更不用考慮如何存儲之類的技術性東西。因為我們現在已經清晰的分析清楚1)場景參與者;2)參與者“做什麼”;代碼如下,應該比較好懂:
/// <summary>
/// 借閱者角色定義
/// </summary>
public interface iborrower : irole<uniqueid>
{
ienumerable<iborrowedbook> borrowedbooks { get; } //借了哪些書
void borrowbook(book book);//借書行為
book returnbook(uniqueid bookid);//還書行為
}
/// 圖書館角色定義
public interface ilibrary : irole<uniqueid>
ienumerable<book> books { get; }//總共有哪些書
book takebook(uniqueid bookid);//書的出庫
void putbook(book book);//書的入庫
/// 被借的書角色定義
public interface iborrowedbook : irole<uniqueid>
book book { get; } //書
datetime borrowedtime { get; }//被借時間
/// 借書場景
public class borrowbookscontext
private ilibrary library;//場景參與者角色1:圖書館角色
private iborrower borrower;//借書參與者角色2:借閱者角色
public borrowbookscontext(ilibrary library, iborrower borrower)
{
this.library = library;
this.borrower = borrower;
}
/// <summary>
/// 啟動借書場景,各個場景參與者開始進行互動
/// </summary>
public void interaction(ienumerable<uniqueid> bookids)
foreach (var bookid in bookids)
{
borrower.borrowbook(library.takebook(bookid));//
}
/// 還書場景
public class returnbookscontext
private ilibrary library;
private iborrower borrower;
public returnbookscontext(ilibrary library, iborrower borrower)
library.putbook(borrower.returnbook(bookid));
接下來考慮角色扮演者如何設計與實作:
角色扮演者就是ddd中的領域對象,在這個例子中主要有:借書卡帳号(libraryaccount)、書本(book)、圖書館(library);下面是這幾個實體類的實作:
public class libraryaccount : object<uniqueid>
#region constructors
public libraryaccount(libraryaccountstate state) : this(new uniqueid(), state)
public libraryaccount(uniqueid id, libraryaccountstate state) : base(id, state)
#endregion
public string number { get; private set; }
public string ownername { get; private set; }
public class book : object<uniqueid>
public book(bookstate state) : this(new uniqueid(), state)
public book(uniqueid id, bookstate state) : base(id, state)
public string bookname { get; private set; }
public string author { get; private set; }
public string publisher { get; private set; }
public string isbn { get; private set; }
public string description { get; private set; }
public class library : object<uniqueid>, ilibrary
private list<book> books = new list<book>();
public library(librarystate state) : this(new uniqueid(), state)
public library(uniqueid id, librarystate state) : base(id, state)
if (state != null && state.books != null)
this.books = new list<book>(state.books);
[mannual]
public ienumerable<book> books
get
return books.asreadonly();
public book takebook(uniqueid bookid)
var book = books.find(b => b.id == bookid);
books.remove(book);
return book;
public void putbook(book book)
books.add(book);
}
以上幾個實體類還有很多細節的東西需要說明,但暫時不是重點。大家可以慢慢體會為什麼我要這樣設計這些類,比如屬性為什麼是隻讀的?
好了,理論上有了角色扮演者、角色,以及場景後,我們就可以寫出借書和還書的完整過程了。代碼如下:
private static void borrowreturnbookexample()
//建立圖書館
var library = new library(null);
repository.add<library>(library);
//建立5本書
var book1 = new book(new bookstate {
bookname = "c#進階程式設計",
author = "jhon smith",
isbn = "56-yaq-23452",
publisher = "清華大學出版社",
description = "a very good book." });
var book2 = new book(new bookstate {
bookname = "jquery in action",
author = "jhon smith", isbn = "09-beh-23452",
publisher = "人民郵電出版社",
var book3 = new book(new bookstate {
bookname = ".net framework programming",
isbn = "12-vtq-96786",
publisher = "機械工業出版社",
var book4 = new book(new bookstate {
bookname = "asp.net professional programming",
author = "jim green",
isbn = "43-wfw-87560",
publisher = "浙江大學出版社",
var book5 = new book(new bookstate {
bookname = "uml and design pattern",
author = "craig larmen",
isbn = "87-opm-44651",
publisher = "微軟出版社",
repository.add<book>(book1);
repository.add<book>(book2);
repository.add<book>(book3);
repository.add<book>(book4);
repository.add<book>(book5);
//将這5本書添加進圖書館
library.putbook(book1);
library.putbook(book2);
library.putbook(book3);
library.putbook(book4);
library.putbook(book5);
//建立一個圖書卡卡号,使用者憑卡号借書,實際過程則是使用者持卡借書
var libraryaccount = new libraryaccount(new libraryaccountstate { number = generateaccountnumber(10), ownername = "湯雪華" });
repository.add<libraryaccount>(libraryaccount);
//建立借書場景并進行場景互動
new borrowbookscontext(
library.actas<ilibrary>(),
libraryaccount.actas<iborrower>()
).interaction(new list<uniqueid> { book1.id, book2.id });
//建立還書場景并進行場景互動
new returnbookscontext(
).interaction(new list<uniqueid> { book1.id });
從上面的高亮代碼中,我們可以清晰的看到領域對象扮演其角色參與到活動。對象在參與活動時因為扮演了某個角色,是以自然也就有了該角色所對應的行為了。但是有人已經想到了,之前我們僅僅隻是定義了角色的接口,并且對象本身也不具備角色所對應的屬性或行為,那麼對象扮演角色時,角色的屬性或行為的具體實作在哪裡呢?這個問題大家自己去看demo的源代碼吧,今天太晚了,眼睛實在快要閉上了。上面我已經把整個場景的參與者角色、角色扮演者、領域對象通過什麼方法扮演(actas)角色、如何觸發場景、領域對象和角色的差別等關鍵問題說明清楚了。而關于如何把角色的行為注入到領域對象之中,我自己思考了很久,思考如何利用c#實作一個既優雅又能確定強類型語言的優勢但同時又能動态将角色的屬性和行為注入到某個對象的設計方式,一切盡在源碼之中!