上篇文章講解了NLayerApp案例的基礎結構層(Cross-Cutting部分),現在,讓我們繼續解讀NLayerApp的基礎結構層(資料訪問部分)。NLayerApp的基礎結構層(資料訪問部分)包含如下內容:Unit Of Work(PoEAA)、倉儲的具體實現、NLayerApp的資料模型以及與測試相關的類。下面,我們將對前三個部分進行討論,與測試相關的內容,我打算最後單獨一章進行介紹。
Unit Of Work(PoEAA)
Unit Of Work(UoW)模式在公司專屬應用程式架構中被廣泛使用,它能夠將Domain Model中對象狀態的變化收集起來,並在適當的時候在同一資料庫連接和交易處理上下文中一次性將對象的變更提交到資料中。在沒有引入UoW之前,你可以在每次增加、刪除對象或者更改對象狀態後,直接調用資料庫以儲存對象的變化,但這樣做會導致應用程式對資料庫這一外部技術架構的頻繁訪問,嚴重影響了系統效能。這就好像我們開啟Notepad進行文字編輯一樣,我們完全可以每輸入一個字元,就按下Ctrl+S儲存一次,但這樣做非常耗時(也沒必要),我們通常的做法可能是,每完成一個段落的編輯(輸入字元、刪除字元或者更改字元等)再儲存一次,那麼Notepad就會在我們編輯段落的時候跟蹤段落及其中字元的變化情況,最後一次性將這些變更寫到硬碟上。從UoW的模式描述上看,它有點像資料庫事務(Transaction),因為它們都具有“提交”和“復原”的操作。但從語義上講,它並不能等同於資料庫事務。我覺得應該這樣理解:我們可以將UoW看成是一個事務對象,但它不是資料庫事務,它的事務性體現在能夠在一個原子操作中將對象一次性提交給持久化機制,或者如果在提交過程中出現問題,它還能將對象返回到提交前的狀態。不僅如此,UoW還具有跟蹤領域對象變化的功能,它能夠跟蹤某一個業務步驟範圍內領域對象的變化情況,正如上面的例子中,每個段落的編輯就可以看成是一個業務步驟,那麼在這個業務步驟中(編輯段落的過程中),UoW會對領域對象進行跟蹤,而在業務步驟完成之時(完成段落編輯之時),UoW就會對跟蹤到的變更做一次性提交。
從上面的分析讓我們大致瞭解到,UoW與倉儲一樣,本身應該是屬於Domain Model的,它的設計應該是技術無關的(也就是常說的POCO或者IPOCO),因為它跟蹤的是Domain Model中領域對象的變化情況;當然,一個更好的設計應該是使用Separated Interface(PoEAA)模式,將UoW介面與倉儲的介面一起設計在Domain Model中。從UoW的實現上來看,NLayerApp採用了Entity Framework的一些特性,並基於Entity Framework的模型,利用T4自動化產生代碼。目前我們不要去關心在NLayerApp中是如何使用T4產生這些代碼的,我們需要關心的是為什麼需要產生這些代碼。有關Visual Studio中的模型項目、Domain Specific Language(DSL)以及T4代碼自動化產生,我們在此將不作討論。有興趣的朋友可以參考我前面的文章《在Visual Studio 2010中使用Modeling Project定製DSL以及自動化代碼產生》。以下是NLayerApp中與UoW相關的類別關係圖:
在瞭解NLayerApp的UoW執行機制之前,首先讓我們瞭解一下NLayerApp中與UoW相關的三個介面。
- IObjectWithChangeTracker介面
該介面下只定義了一個ObjectChangeTracker的屬性,在NLayerApp中,所有的實體都要實現IObjectWithChangeTracker介面,以向外界(主要是UoW和倉儲)提供ObjectChangeTracker執行個體。ObjectChangeTracker的主要功能就是記錄當前實體中的狀態變化。比如,實體的目前狀態、變更前所有屬性的未經處理資料、向集合屬性添加的所有對象、從集合屬性中刪除的所有對象等等。當倉儲通過Unit Of Work來註冊已變更的實體時,Unit Of Work會使用ObjectChangeTracker所提供的資訊來向Entity Framework進行變更註冊。
- INotifyPropertyChanged介面
NLayerApp的實體不僅實現了IObjectWithChangeTracker介面,同時還實現了INotifyPropertyChanged介面。實現這個介面的主要目的就是為了在實體的某個屬性發生變化時,能及時地將這種變化記錄在ObjectChangeTracker中。因此,只要客戶程式通過實體的屬性來改變實體的狀態時,實體本身就會將狀態變化記錄到ObjectChangeTracker中。
- IRepository介面
IRepository介面是定義在Domain Model層的介面,之所以在此提及,是因為對象的持久化過程是通過倉儲完成的,而持久化又離不開UoW。在NLayerApp中,IRepository介面有一個IUnitOfWork的屬性,因此所有的倉儲都必須實現這個屬性,以便Repository能夠在UoW中記錄對象的變更資訊。從NLayerApp的原始碼可以看到,其實倉儲本身並不負責將實體儲存到資料庫的這一具體任務,它只是通過IObjectWithChangeTracker介面,將需要儲存的對象設定為相應的狀態,並向UoW註冊對象變更;剩下的與資料庫打交道的任務,則是由UoW完成的
通過這些資訊我們可以瞭解到,NLayerApp中的實體都是各自管理自己的變更記錄,稱之為“自我追蹤實體”(Self-Tracking Entities,STE)。其實從DDD的角度來看,STE並不是一個很好的設計,因為它給Domain Model帶來了太多技術關注點。例如在實現STE的時候,當你向Customer添加一個Order時,你需要首先判斷Customer的ObjectChangeTracker中是否已經將該Order標記為“刪除”狀態了,如果是這樣的話,那麼你需要將這個Order從ObjectChangeTracker的“刪除”列表中移去。類似這樣的商務邏輯本不應該放在Domain Model中。此外,NLayerApp為了迎合Entity Framework的需求,所實現的STE也並非純粹的與技術無關的。UoW的實現也是如此,比如在上面的類圖中,我們可以很明顯地看到,MainModuleUnitOfWork是ObjectContext的子類。
現在我們將思路串聯起來,以修改Customer為例,從整個架構服務端的最上層(Distributed Service層)開始,看看Unit Of Work與倉儲是如何協作的。
1、DistributedServices.MainModule項目:MainModuleService類通過使用位於應用程式層的CustomerManagementService實現Customer資訊的變更:
public void ChangeCustomer(Customer customer){ try { //Resolve root dependency and perform operation ICustomerManagementService customerService = IoCFactory .Instance .CurrentContainer.Resolve<ICustomerManagementService>(); customerService.ChangeCustomer(customer); } catch (ArgumentNullException ex) { // ...... }}
上述代碼通過IoCFactory從IoC容器中獲得ICustomerManagementService的具體實現,有關NLayerApp中IoC容器的實現,請參考前一篇文章。
2、Application.MainModule項目:CustomerManagementService類實現了ICustomerManagementService介面,同時實現了ChangeCustomer方法。在該方法中,首先通過CustomerRepository的UnitOfWork屬性獲得UoW,然後調用倉儲的Modify方法以將要更改的Customer實體註冊到UoW中,同時改變了Customer實體的狀態。最後,使用UoW的CommitAndRefreshChanges方法將變更的實體物件提交到資料庫:
public void ChangeCustomer(Customer customer){ if (customer == (Customer)null) throw new ArgumentNullException("customer"); IUnitOfWork unitOfWork = _customerRepository.UnitOfWork as IUnitOfWork; _customerRepository.Modify(customer); unitOfWork.CommitAndRefreshChanges();}
值得一提的是,在CustomerManagementService中,CustomerRepository以構造器注入的方式獲得執行個體化的:
/// <summary>/// Create new instance /// </summary>/// <param name="customerRepository">Customer repository dependency, /// intented to be resolved with dependency injection</param>/// <param name="countryRepository">Country repository dependency, /// intended to be resolved with dependency injection</param>public CustomerManagementService(ICustomerRepository customerRepository, ICountryRepository countryRepository){ if (customerRepository == (ICustomerRepository)null) throw new ArgumentNullException("customerRepository"); if (countryRepository == (ICountryRepository)null) throw new ArgumentNullException("countryRepository"); _customerRepository = customerRepository; _countryRepository = countryRepository;}
3、Infrastructure.Data.Core項目:Repository類的Modify方法首先將目前狀態不是Deleted的實體設定為“Modified”,同時在UoW中,通過RegisterChanges調用以向UoW註冊該實體:
public virtual void Modify(TEntity item){ //check arguments if (item == (TEntity)null) throw new ArgumentNullException("item", Resources.Messages.exception_ItemArgumentIsNull); //Set modifed state if change tracker is enabled and state is not deleted if (item.ChangeTracker != null && ((item.ChangeTracker.State & ObjectState.Deleted) != ObjectState.Deleted) ) { item.MarkAsModified(); } //apply changes for item object _CurrentUoW.RegisterChanges(item); _TraceManager.TraceInfo( string.Format(CultureInfo.InvariantCulture, Resources.Messages.trace_AppliedChangedItemRepository, typeof(TEntity).Name));}
4、Infrastructure.Data.MainModule項目:MainModuleUnitOfWork類的RegisterChanges方法簡單地利用Entity Framework所提供的機制,向Entity Framework註冊對象狀態變更。這是Entity Framework技術實現的細節內容,我們在此也不去深入分析其中的實現方式了:
public void RegisterChanges<TEntity>(TEntity item)where TEntity : class, IObjectWithChangeTracker{this.CreateObjectSet<TEntity>().ApplyChanges(item);}
5、Infrastructure.Data.MainModule項目:MainModuleUnitOfWork類的CommitAndRefreshChanges方法通過Entity Framework將變更提交到資料庫,同時將實體物件的狀態設定為“未更改”:
public void CommitAndRefreshChanges(){ try { //Default option is DetectChangesBeforeSave base.SaveChanges(); //accept all changes in STE entities attached in context IEnumerable<IObjectWithChangeTracker> steEntities = (from entry in this.ObjectStateManager .GetObjectStateEntries(~EntityState.Detached) where entry.Entity != null && (entry.Entity as IObjectWithChangeTracker != null) select entry.Entity as IObjectWithChangeTracker); steEntities.ToList().ForEach(ste => ste.MarkAsUnchanged()); } catch (OptimisticConcurrencyException ex) { //...... }}
整個執行過程我們可以使用下面的順序圖表來表示:
NLayerApp中的Unit Of Work我們先介紹到這裡,有疑問的朋友可以以評論的方式交流。
倉儲的具體實現
NLayerApp中的倉儲實現也是基礎結構層(資料訪問部分)的一個重要組件,這一點與DDD的經典架構風格是相符的。因為從理論上講,倉儲的具體實現需要依賴於外部系統,而這部分內容是不能暴露給Domain Model層的,也就是我們平時所說的,需要做到Persistence Ignorance。NLayerApp首先為所有實體(確切地說應該是彙總根)設計了一個通用的泛型倉儲,你可以在Infrastructure.Data.Core項目中找到這個泛型倉儲的原始碼,它實現了一個倉儲應具有的所有準系統,比如添加、刪除、修改實體物件以及基於規約的一些查詢操作等;然後,針對某些彙總根,NLayerApp會根據項目的實際需求,在倉儲中實現一些特定的操作。比如:CustomerRepository繼承於Repository這個通用倉儲,同時實現了ICustomerRepository介面,以向外界提供通過規約(Specification)來尋找Customer資訊的功能。這樣的設計在一定程度上做到了關注點分離,比如當我們對實體進行通用的倉儲操作時,我們只需要獲得IRepository介面的具體實現即可,而無需使用ICustomerRepository來獲得與Customer有關的倉儲實現。有關ICustomerRepository與關注點分離的相關內容,我將在下一講(領域模型層)進行講解。
以下是NLayerApp中倉儲的類別關係圖,在此貼出以供讀者參考。
NLayerApp的倉儲實現也使用了不少與Entity Framework相關的技術細節,比如ObjectSet等,這些都是具體技術實現上的內容,在此就不多作介紹了。有興趣的讀者請參考與Entity Framework技術相關的資料文檔。
NLayerApp的資料模型
NLayerApp使用Entity Framework的ADO.NET Entity Data Model設計器來設計資料模型,這使得我們能夠對整個Domain Model的對象結構有一個很直觀的認識。該資料模型位於Infrastructure.Data.MainModule項目下,直接雙擊MainModuleDataModel.edmx就可以在設計器中開啟,對象結構及其之間的關係就能很清楚地展現在你面前。你會發現,其實在這個資料模型的後台代碼檔案中,除了一些注釋以外,並沒有任何實質性內容,這是因為NLayerApp僅僅是利用這個設計器來設計資料模型,而真正的Domain Model的代碼則會在Domain Model層中,根據該資料模型,利用T4進行自動化產生,詳情請見Domain.MainModule.Entities項目。這也使得我們會去思考這樣一個糾結的問題:Entity Framework為我們提供的,到底是一個面向資料庫設計的資料模型,還是面向領域驅動的領域模型?或許在實際應用中,我們更多地是將其放在ORM的位置上,於是Entity Data Model就變成了位於Domain Model實體物件與資料庫之間的行資料入口(Row Data Gateway,PoEAA)。之前我對於基於Entity Framework的領域驅動設計實踐也寫過一些文章,讀者朋友可以參考《領域驅動設計系列文章匯總》。
總結
本文對NLayerApp的基礎結構層(資料訪問部分),尤其是Unit Of Work的實現進行了分析與介紹;下一講開始,我們將一起學習NLayerApp的Domain Model部分。
參考閱讀
- Unit Of Work模式:《公司專屬應用程式架構模式(PoEAA)》
- Separated Interface模式:《公司專屬應用程式架構模式(PoEAA)》
- Row Data Gateway模式:《公司專屬應用程式架構模式(PoEAA)》
- Repository模式:《公司專屬應用程式架構模式(PoEAA)》
- 關注點分離(Separation of Concerns)