本文將重點介紹Microsoft NLayerApp的領域模型層,這涉及到Domain.Core、Domain.Core.Entities、Domain.MainModule以及Domain.MainModule.Entities四個項目。Domain.Core項目包含了基本介面的定義以及規約模式(Specification Pattern)的實現;Domain.Core.Entities則包含了支援Entity Framework的STE(Self-Tracking Entity)的實現代碼,在上文《Microsoft NLayerApp案例理論與實踐 - 基礎結構層(資料訪問部分)》我對STE做了一些介紹,但它的實現與Entity Framework(EF)結合的比較緊密,EF超出了本系列文章的討論範圍,因此,我們也不會針對STE的具體實現方式做太多討論;Domain.MainModule根據項目需求,針對不同的實體定義了倉儲介面,同時實現了項目所需的規約類型。領網域服務也是該項目的重要部分;Domain.MainModule.Entities項目中包含了NLayerApp領域模型的核心代碼。本文將從倉儲介面、規約、領網域服務、領域模型這四個方面對NLayerApp的Domain Model層做一個簡單的介紹。
倉儲介面
根據我們在《Microsoft NLayerApp案例理論與實踐–DDD、分布式DDD及其分層》一文中的討論,倉儲的具體實現是放在基礎結構層的,而倉儲的介面則是放在領域模型層的。Domain.Core項目的IRepository介面就是倉儲介面,所有的倉儲類都需要實現該介面中定義的屬性與方法。在Domain.Core項目下還有一個繼承IRepository介面的IExtendedRepository介面,它包含了一些額外的方法來擴充IRepository的功能。事實上在整個NLayerApp中並沒有真正用到IExtendedRepository介面,因此我們也不在此做過多討論。是NLayerApp中與倉儲的介面和實現相關的類別關係圖,為了方便瀏覽和描述,該圖中僅包含了Customer倉儲的定義與實現部分:
首先,ICustomerRepository介面繼承於IRepository介面,以擴充IRepository來定義特定於Customer實體的倉儲。因此,所有實現ICustomerRepository介面的類,不僅具備倉儲的準系統,而且還具有特定於Customer實體的倉儲操作。其次,Repository類實現了IRepository介面,並作為所有倉儲實現的基類,實現了IRepository介面中定義的方法,它在倉儲部分的角色就是一個層超類型(Layer Supertype)。最後,CustomerRepository類繼承於Repository類,同時實現了ICustomerRepository介面,由於Repository類中已經實現了IRepository中定義的所有方法,因此CustomerRepository類就無需去實現這些方法,只需要把關注點放在ICustomerRepository的實現上即可。以下是位於基礎結構層的CustomerRepository代碼,供讀者朋友參考:
public class CustomerRepository :Repository<Customer>,ICustomerRepository{ #region Constructor /// <summary> /// Default constructor /// </summary> /// <param name="traceManager">Trace manager dependency</param> /// <param name="unitOfWork">Specific unitOfWork for this repository</param> public CustomerRepository(IMainModuleUnitOfWork unitOfWork, ITraceManager traceManager) : base(unitOfWork, traceManager) { } #endregion #region ICustomerRepository implementation /// <summary> /// <see cref="Microsoft.Samples.NLayerApp.Domain.MainModule.Customers.ICustomerRepository"/> /// </summary> /// <param name="specification"> /// <see cref="Microsoft.Samples.NLayerApp.Domain.MainModule.Customers.ICustomerRepository"/> /// </param> /// <returns>Customer that match <paramref name="specification"/></returns> public Customer FindCustomer(ISpecification<Customer> specification) { //validate specification if (specification == (ISpecification<Customer>)null) throw new ArgumentNullException("specification"); IMainModuleUnitOfWork activeContext = this.UnitOfWork as IMainModuleUnitOfWork; if (activeContext != null) { //perform operation in this repository return activeContext.Customers .Include(c => c.CustomerPicture) .Where(specification.SatisfiedBy()) .SingleOrDefault(); } else throw new InvalidOperationException(string.Format( CultureInfo.InvariantCulture, Messages.exception_InvalidStoreContext, this.GetType().Name)); } #endregion}
正如所述,ICustomerRepository介面擴充了IRepository介面以提供與Customer有關的倉儲操作。對於應用程式開發架構來說,這樣的設計有助於提高系統的擴充性。比如之前有網友針對Apworks架構提問,覺得Apworks的倉儲介面只提供了一些很基本的操作,但他希望能夠在倉儲上增加一些諸如分頁查詢對象的操作,之前他的設計是,另外定義一個介面(IFooRepository),其中添加一些分頁查詢操作,然後讓倉儲執行個體同時實現IRepository和IFooRepository。如下:
這樣做看上去FooRepository是一個完整的倉儲實現,但IFooRepository與IRepository之間沒有任何聯絡,IFooRepository本身並沒有體現“倉儲”的語義,但它原本就是一種倉儲。從實踐上看,我們需要在IoC容器中分別為IRepository和IFooRepository註冊相同的類型:FooRepository,以便在程式中能夠正確地解析IRepository和IFooRepository的具體實現,從而通過IRepository或者IFooRepository分別獲得不同的倉儲操作。當然,對於我們目前的情形,FooRepository同時實現IRepository和IFooRepository介面,那麼C#是可以通過as關鍵字將該執行個體在IRepository和IFooRepository的執行個體間進行轉換的,比如:
IContainer container = IoCFactory.Instance.CurrentContainer;using (IRepositoryContext ctx = container.Resolve<IRepositoryContext>()){ IRepository<Foo> repository = ctx.GetRepository<Foo>(); // do sth. with repository ... IFooRepository<Foo> fooRepository = repository as IFooRepository<Foo>(); if (fooRepository != null) // this is required... { // do sth. with fooRepository }}
但是在應用程式開發的過程中,我們無法去約束開發人員一定要讓FooRepository去實現IFooRepository介面,這就造成了上面的類型轉換不成功,因此,判斷fooRepository執行個體是否為空白就顯得非常重要。
這樣的設計還有另外一個缺陷,就是由於IFooRepository沒有體現“倉儲”的語義,這就導致它無法應用到基於倉儲的類型約束上。例如,假設根據需求我們需要用到一個介面IMyInterface,它的定義如下:
interface IMyInterface<T, S> where T : IRepository<S> where S : class{ }
那麼很明顯我們就無法去定義一個類,在這個類中通過泛型參數T來使用IFooRepository介面:
// error:class MyClass : IMyInterface<IFooRepository<MyEntity>, MyEntity>{ }
相比之下,NLayerApp用了一個從語義上來講更為合理的設計(如),它充分體現了“IFooRepository是一種倉儲”的概念,總之,兩種不同的設計的主要區別在各自所表達的物件導向語義上。
規約(Specification)
在Domain.Core項目下,NLayerApp定義了應用程式領域模型層所需要用到的規約架構,主要是通過LINQ Expression來實現的。在ISpecification介面中定義了SatisfiedBy方法,該方法返回一個LINQ Expression,用來執行判斷領域對象是否能夠滿足當前規約條件的邏輯。NLayerApp的規約結構如所示:
有關規約模式,請參見:《Specifications》、《Specification Pattern》;有關規約模式、應用情境以及支援LINQ Expression的.NET規約實現,請參見:《EntityFramework之領域驅動設計實踐(十):規約(Specification)模式》。本文就不再重複這些內容了。
值得一提的是,NLayerApp的規約實現,在Specification抽象類別中重載了一些邏輯運算子,這使得在實際應用中使用規約變得非常方便。
領網域服務(Domain Services)
在DDD中,“服務”的概念得到了擴充,它表示在任何層中,包含了這樣一種操作的類型,這種操作從邏輯上無法歸結到任何對象上。因此“服務”並不僅僅是應用程式層或者基礎結構層的專利,領域模型中也存在服務。在我的《EntityFramework之領域驅動設計實踐【擴充閱讀】:服務(Services)》一文中,對領網域服務做了簡單的介紹,供讀者朋友參考。就NLayerApp而言,它實現了一個Bank Transfer的服務,首先定義了IBankTransferDomainService的介面,然後由BankTransferDomainService實現該介面。服務執行的參與者就是兩個BankAccount實體,參數就是需要轉賬的金額。在Application層,BankingManagementService的PerformTransfer方法就使用了該服務來實現銀行賬戶轉賬。
領域模型(Domain Model)
之前我也提到過,NLayerApp的領域模型是根據Entity Framework的Data Model,通過T4自動產生的,代碼中除了包含了Data Model本身所定義的對象屬性及對象間的關係外,還包含了基於Entity Framework實現STE的代碼。從嚴格上講,這並不是一個純淨的領域模型,其中STE的實現牽涉到了很多技術(而非領域)實現細節;此外,所有的領域對象都被DataContract修飾,也就意味著它們將同時以DTO的身份穿梭在網路中。NLayerApp的官方資料中對這種實現有過說明,解釋過這種做法並不是很好的DDD實踐,但它能夠適用於NLayerApp。另外,NLayerApp採用C#的partial關鍵字向領域對象中添加了業務方法,Domain.MainModule.Entities項目下Partial子目錄中包含了這些代碼,比如在Order實體上實現了GetNumberOfItems操作。這一點與我以前在《EntityFramework之領域驅動設計實踐 (一):從DataTable到EntityObject》一文中討論的思路是相同的。在此,我們也不對NLayerApp的領域邏輯實現過程做太多介紹,有問題的朋友可以通過留言進行討論。
總結
本文對NLayerApp的領域模型層做了簡單的介紹,尤其對倉儲介面的設計做了詳細討論。下篇文章我將介紹NLayerApp的應用程式層。