案例:一個簡易的銷售系統
從現在開始,我們將以一個簡易的銷售系統為例,探討EntityFramework在領域驅動設計上的應用。為了方便討論,我們的銷售系統非常簡單,不會涉及客戶存在多個收貨地址的情況,也不會包含任何庫存管理的內容。假設我們的系統只需要維護產品類型、產品以及客戶資訊,並能夠幫客戶下訂單、跟蹤訂單狀態,以及接受客戶退貨。從簡單的分析我們大致可以瞭解到,這個系統將會有如下實體:客戶、單據、產品及其類型。單據分為銷售訂單和退貨單兩種,每個單據可以有多個單據行(比如銷售訂單行和退貨單行)。不僅如此,系統允許每個客戶有多張信用卡,以便在結賬的時候,選擇一張信用卡進行支付。在使用EF的Entity Data Model Designer進行設計後,我們得到下面的模型:
上面的模型表述了領域模型中各個實體及其之間的關係。我們先不去討論整個系統的業務會是什麼樣的,我們先看看EF是如何支援實體和值對象概念的。
實體
首先看看實體這個概念。在領域驅動設計的理論中,實體是模型中需要區分個體的對象,也就是說,針對某種類型,我們既要知道它是什麼,還需要知道它是哪個。我在前面的博文中有介紹過實體這個概念。實體都有一個標識符,以便跟同類型的其它實體進行區分。EF Entity Data Model Designer上能夠畫出的都是實體,你可以看到每個實體都有個Id成員,其Entity Key屬性被設定為True,同時被分配了一種標識符的產生方式,如所示:
在從領域模型映射到資料模型的過程中,這個標識符通常都是被映射為關聯式資料庫某個資料表的主鍵,這個應該是很容易理解的。
其次,EF不支援實體行為,因此,整個模型只能被稱為Entity Data Model,而不是Entity Model,因為它只支援對實體資料的描述。幸虧從.NET 2.0開始,託管語言開始支援partial特性,同一個類可以以部分類(partial class)的特性寫入多個代碼檔案中。因此,如果需要向上述模型中的實體加入行為,我們可以在工程中加入幾個代碼檔案,然後使用部分類的特點,為實體添加必要的行為。比如,下面的部分類向訂單行中加入了一個唯讀屬性,該屬性用於計算某一單據行所擁有的總金額:
有朋友會問,為什麼我們要另外使用部分類,而不是直接在模型檔案 edmx的原始碼上直接修改?因為這個原始碼檔案是架構動態產生的,如果在上面修改,等下次模型被更新的時候,你所做的更改便會丟失。
對於實體的行為,EF支援從資料庫預存程序產生實體物件行為的過程。對此,我持批判態度:EF把資料模型與實體模型混為一談了,這種做法只能讓軟體人員感到更加困惑。我在下一篇文章將重點表述我對這個問題的看法。我也相信微軟在下一代Entity Framework中能夠處理好這個問題。
再次,EF對實體物件關係的支援主要有關聯和繼承。根據Multiplicity的設定,關聯又可以支援到組合關聯與彙總關聯。我覺得EF中對繼承關係的支援是一個亮點。繼承表述了“什麼是一種什麼”的觀點,比如在我們的案例中,“銷售訂單”和“退貨單”都是一種“單據”。如果從傳統的資料庫驅動的設計方案,我們很自然地會使用“Orders”資料表中的整型欄位“OrderType”來儲存當前單據的類型(比如0表示銷售訂單,1表示退貨單),那麼,在擷取系統中所有銷售訂單的時候,我們會使用下面的代碼:
隱藏行號 複製代碼 ? 擷取所有銷售訂單的虛擬碼
List<Order> GetSalesOrders(IDbConnection connection)
{
IDbCommand command = new SqlCommand("SELECT * FROM [Orders] WHERE [OrderType]=0",
(SqlConnection)connection);
List<Order> orders = new List<Order>();
using (IDataReader dr = command.ExecuteReader())
{
while (dr.Read())
{
Order order = new Order();
order.Id = Convert.ToInt32(dr["Id"]);
// ...
orders.Add(order);
}
dr.Close();
}
return orders;
}
從技術角度講,上面的代碼沒什麼問題,啟動並執行也很好,能夠獲得系統中所有銷售訂單的列表。但是, [OrderType]=0這種寫法並不包含任何領域語義,如果讓另一個開發人員來跟進這段代碼,他不得不先去查閱其它項目文檔,以瞭解這個 [OrderType]=0的具體涵義。在引入了繼承關係的EF中,我們只需要下面的Linq to Entities,即可既方便、又明了地獲得所有銷售訂單的列表了:
隱藏行號 複製代碼 ? 使用LINQ to Entities後的代碼
List<Order> GetSalesOrders()
{
using (EntitiesContainer container = new EntitiesContainer())
{
return (from order in container.Orders
where order is SalesOrder
select order).ToList();
}
}
簡單明了吧?EF帶給我們的不僅僅是一個技術架構,也不僅僅是一個資料存取的解決方案。
值對象
EF支援值對象,這很好!在EF中可以定義Complex Types,而一個Complex Type下可以定義多個Primitive Type和多個Complex Type。與LINQ to SQL相比,這是一大進步。
對於值對象的兩點問題我在第一篇文章中已經講過了,在此就不重複了。
綜上所述,EF基本上能夠支援領域驅動設計的思想(即使有些方面不完善,但目前也可以找到替代的方案)。我想,只要能夠對領域驅動有清晰的認識,就能夠很好地將Entity Framework應用於領域驅動的實踐中。