分享一些如何分析和設計具有動態行為的領域模型的經驗

作者: netfocus  來源: 博客園  發布時間: 2012-01-19 17:19  閱讀: 2013 次  推薦: 2   原文鏈接   [收藏]  

  好久沒有寫文章了,最近比較忙,另一方面也是感覺自己在這方面沒什么實質性的突破。但是今天終于感覺自己小有所成,有些可以值得和大家分享的東西,并且完成了兩個可以表達自己想法的Demo。因此,趁現在有點時間,是寫文章和大家分享的時候了。

  首先給出這兩個Demo的源代碼的壓縮包的下載地址,因為之前有博友說他沒有裝VS2010而沒辦法運行Demo,所以這次我分別用VS2008和VS2010實現了兩個版本。

  http://files.cnblogs.com/netfocus/DCIBasedDDD.rar

  下面先分享一下我最近研究的一些知識及我對這些知識的自我感悟,然后再結合Demo中的示例講解如何將這些感悟應用到實際。 

  一、理論知識:

  我最近一直在學習下面這些東西:

  1. 面向對象分析與設計,即Object Oriented Analysis and Design(OOA\D)
  2. 領域驅動設計,即Domain Driven Design(DDD)
  3. 四色原型:MI原型、Role原型、PPT原型、Description原型
  4. DCI架構:Data Context Interaction
  5. CQRS架構: 命令查詢職責分離原則,即Command Query Responsibility Segregation

  通過學習以上這些知識,讓我對面向對象的分析、設計、實現有了一些新的認識。

  1. 碰到一個業務系統,我們該如何分析業務,分析需求,并最后得到一個只包含業務概念的模型?答案是通過四色原型進行業務建模。四色原型的中心思想是:一個什么什么樣的人或組織或物品或地點以某種角色在某個時刻或某段時間內參與某個活動。 其中“什么什么樣的”就是DESC,“人或組織或物品或地點”就是PPT,“角色”就是Role,而”某個時刻或某段時間內的某個活動"就是MI。更具體的說明請參看我之前整理的一篇文章:四色原型的學習心得分享

  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)角色就是借書活動的參與者。

  二、結合Demo講解如何將理論應用到實際:

  前面的介紹看起來比較枯燥,但對我來說是非常寶貴的經驗積累。下面我通過一個例子分析如何運用這些知識:

  以圖書管理系統中的借書和還書的場景進行說明:

  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);//還書行為
}
///<summary>
/// 圖書館角色定義
///</summary>
public interface ILibrary : IRole<UniqueId>
{
IEnumerable
<Book> Books { get; }//總共有哪些書
Book TakeBook(UniqueId bookId);//書的出庫
void PutBook(Book book);//書的入庫
}
///<summary>
/// 被借的書角色定義
///</summary>
public interface IBorrowedBook : IRole<UniqueId>
{
Book Book {
get; } //
DateTime BorrowedTime { get; }//被借時間
}
///<summary>
/// 借書場景
///</summary>
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));
//
    }
}
}

///<summary>
/// 還書場景
///</summary>
public class ReturnBooksContext
{

private ILibrary library;
private IBorrower borrower;

public ReturnBooksContext(ILibrary library, IBorrower borrower)
{

this.library = library;
this.borrower = borrower;
}

public void Interaction(IEnumerable<UniqueId> bookIds)
{

    foreach (var bookId in bookIds)
    {
        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; privateset; }
public string OwnerName { get; privateset; }
}

public class Book : Object<UniqueId>
{
#region Constructors

public Book(BookState state) : this(new UniqueId(), state)
{
}

public Book(UniqueId id, BookState state) : base(id, state)
{
}


#endregion

public string BookName { get; private set; }
public string Author { get; private set; }
public string Publisher { get; private set; }
publicstring ISBN { get; private set; }
publicstring Description { get; private set; }
}

publicclass 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;
}


publicvoid 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
"人民郵電出版社",
Description
"A very good book." });
var book3
new Book(new BookState {
BookName
".NET Framework Programming",
Author
"Jhon Smith",
ISBN
"12-VTQ-96786",
Publisher
"機械工業出版社",
Description
"A very good book." });
var book4
new Book(new BookState {
BookName
"ASP.NET Professional Programming",
Author
"Jim Green",
ISBN
"43-WFW-87560",
Publisher
"浙江大學出版社",
Description
"A very good book." });
var book5
new Book(new BookState {
BookName
"UML and Design Pattern",
Author
"Craig Larmen",
ISBN
"87-OPM-44651",
Publisher
"微軟出版社",
Description
"A very good book." });
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(
library.ActAs
<ILibrary>(),
libraryAccount.ActAs
<IBorrower>()
).Interaction(
new List<UniqueId> { book1.Id });

}

  從上面的高亮代碼中,我們可以清晰的看到領域對象扮演其角色參與到活動。對象在參與活動時因為扮演了某個角色,因此自然也就有了該角色所對應的行為了。但是有人已經想到了,之前我們僅僅只是定義了角色的接口,并且對象本身也不具備角色所對應的屬性或行為,那么對象扮演角色時,角色的屬性或行為的具體實現在哪里呢?這個問題大家自己去看Demo的源代碼吧,今天太晚了,眼睛實在快要閉上了。上面我已經把整個場景的參與者角色、角色扮演者、領域對象通過什么方法扮演(ActAs)角色、如何觸發場景、領域對象和角色的區別等關鍵問題說明清楚了。而關于如何把角色的行為注入到領域對象之中,我自己思考了很久,思考如何利用C#實現一個既優雅又能確保強類型語言的優勢,但同時又能動態將角色的屬性和行為注入到某個對象的設計方式,一切盡在源碼之中!

2
0
 
標簽:DDD
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()