案例:一個簡易的銷售系統
從現在開始,我們將以一個簡易的銷售系統為例,探討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把數據模型與實體模型混為一談了,這種做法只能讓軟件人員感到更加困惑。我在下一篇文章將重點表述我對這個問題的看法。我也相信微軟在下一代實體框架中能夠處理好這個問題。
再次,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,即可既方便、又明了地獲得所有銷售訂單的列表了:
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基本上能夠支持領域驅動設計的思想(即使有些方面不完善,但目前也可以找到替代的方案)。我想,只要能夠對領域驅動有清晰的認識,就能夠很好地將實體框架應用於領域驅動的實踐中。