Tiny Library使用應用服務層向使用者介面層提供服務,具體實現是採用Microsoft WCF Services。在Tiny Library的解決方案中,是由TinyLibrary.Services項目為整個系統提供這一WCF服務的。按照傳統的應用系統分層方法,TinyLibrary.Services項目位於領域模型層之上、使用者介面層之下,它是UI與Domain的互動介面。TinyLibrary.Services的實現中,與DDD相關的內容主要是資料轉送對象(DTO),至於如何編寫與實現WCF服務,那是.NET技術上的問題,本文不會做太多的討論。
資料轉送對象(Data Transferring Object,DTO)
在TinyLibrary.Services中,有一種特殊類型的對象,我們稱之為資料轉送對象。根據Fowler在PoEAA一書中的描述,DTO用於進程間資料交換,由於DTO是一種對象,它能夠包含很多資料資訊,因此,DTO的採用可以有效減少進程間資料交換的來回次數,從而在一定程度上提高網路傳輸效率。
由於DTO需要跨越進程邊界(在我們的案例中,需要跨越網路邊界),因此,DTO是可以被序列化/還原序列化的,它不能包含上下文相關的資訊(比如,Windows控制代碼)。正因為如此,DTO中的資料類型都是很簡單的,它們可以是未經處理資料類型(Primitive Data Types)或者是其它的DTO。
有些朋友在閱讀Fowler的PoEAA一書時,對於DTO的理解還是有困難,我在此將我的理解寫下來,供大家參考
- 領域模型對象不負責任何資料轉送的功能,這是因為,如果將領域模型對象用於資料轉送,則勢必需要在另一個層面(或者另一個進程中)產生一個領域模型對象的副本,此時才有可能在伺服器與客戶機之間產生一種資料契約,而這種做法違背了層內高內聚,層間低耦合的原則
- DTO是簡單而原始的,並且是運行時上下文無關的。這是為了滿足序列化/還原序列化的需求。因此,DTO所包含的資料要不就是未經處理資料類型,要不就是其它的DTO
- 用戶端需求決定DTO的設計。因此,DTO與領域模型對象並非一一對應的關係,相反,它是為了滿足用戶端需求而存在的
在Tiny Library案例中,我選用了WCF的Data Contracts作為DTO的實現標準,因為這樣做不僅能夠基於用戶端需求設計合理的DTO結構,而且還可以利用WCF的DataContractSerializer實現DTO的序列化/還原序列化。不僅如此,WCF為我們在伺服器與客戶機通訊上提供了支援人員和有力保障。
開啟TinyLibrary.Services項目,我們可以看到一個IDataObject的泛型介面,其定義如下:
隱藏行號 複製代碼 ? IDataObject定義
-
public interface IDataObject<TEntity> where TEntity : IEntity
-
{
-
void FromEntity(TEntity entity);
-
TEntity ToEntity();
-
}
-
我們可以要求所有的DTO都實現這個介面,以便使其具有將實體轉換為DTO,或者將DTO轉換為實體的能力。在Tiny Library案例中,我並沒有強制要求所有的DTO都實現這個介面(也就是說,Tiny Library本身不規定DTO必須實現這個介面),我引入這個介面的目的,就是想說明,其實我們是可以這樣做的:在我們的系統中為DTO設計好一個合理的架構,以便讓DTO獲得更強大的功能。引入這個介面的另一個目的就是為了編程方便:實現基於某個實體的DTO,只需要使其實現IDataObject介面即可,Visual Studio會自動產生方法樁(Method Stubs),無需手動編寫代碼。
請注意TinyLibrary.Services.DataObjects.RegistrationData這個類,它與BookData、ReaderData有很大的區別,它並不是Registration實體的映射體現,換句話說,它所包含的所有狀態屬性,並不是與Registration實體所包含的屬性一一對應。例如,RegistrationData這個DTO中包含書名(BookTitle)以及ISBN號(BookISBN)的資訊,而這些資訊都是來自於Registration的關聯實體:Book。RegistrationData的設計完全是為了迎合使用者介面,因為在UI上,我們需要針對某個讀者列出他/她的借書資訊,而RegistrationData包含了這所有需要的資訊。
應用程式層職責
在《Entity Framework之領域驅動設計實踐》系列文章中,我曾經提到過,根據DDD,應用系統分為四層:展現層、應用程式層、領域層和基礎結構層。在Tiny Library案例中,TinyLibrary.Services充當了應用程式層的職責,它不負責處理任何商務邏輯,而是從更高的層面,為商務邏輯的正確執行提供適當的運行環境,同時起到任務協調的作用(比如交易處理和基礎結構層服務調用)。
隱藏行號 複製代碼 ? WCF Service中“還書”操作的具體實現
-
public void Return(string readerUserName, Guid bookId)
-
{
-
try
-
{
-
using (IRepositoryTransactionContext ctx = ObjectContainer
-
.Instance
-
.GetService<IRepositoryTransactionContext>())
-
{
-
IRepository<Book> bookRepository = ctx.GetRepository<Book>();
-
IRepository<Reader> readerRepository = ctx.GetRepository<Reader>();
-
Reader reader = readerRepository.Find(Specification<Reader>.Eval(r => r.UserName.Equals(readerUserName)));
-
Book book = bookRepository.GetByKey(bookId);
-
reader.Return(book);
-
ctx.Commit();
-
}
-
}
-
catch
-
{
-
throw;
-
}
-
}
-
上面的代碼展示了“還書”操作的具體實現方式,我們可以看到,位於應用程式層的WCF Services僅僅是協調倉儲操作和交易處理,商務邏輯由reader.Return方法實現:
隱藏行號 複製代碼 ? TinyLibrary.Domain.Reader的Return方法
-
public void Return(Book book)
-
{
-
if (!book.Lent)
-
throw new InvalidOperationException("The book has not been lent.");
-
var q = from r in this.Registrations
-
where r.Book.Id.Equals(book.Id) &&
-
r.RegistrationStatus == RegistrationStatus.Normal
-
select r;
-
if (q.Count() > 0)
-
{
-
var reg = q.First();
-
if (reg.Expired)
-
{
-
// TODO: Reader should pay for the expiration.
-
}
-
reg.ReturnDate = DateTime.Now;
-
reg.RegistrationStatus = RegistrationStatus.Returned;
-
book.Lent = false;
-
}
-
else
-
throw new InvalidOperationException(string.Format("Reader {0} didn't borrow this book.",
-
this.Name));
-
}
-
設定檔
TinyLibrary.Services是整個案例的服務供應者(Service Provider),因此,整個系統伺服器端的配置與初始化應該由TinyLibrary.Services啟動時負責執行。因此,基於伺服器的系統配置應該寫在TinyLibrary.Services項目的app.config中。其中包括:Apworks的配置、Unity(或者Castle Windsor)的配置、WCF Services的配置以及Entity Framework所使用的資料庫連接字串的設定。
從實踐角度考慮,在基於CQRS體繫結構模式的應用系統中,各個組件的初始化和配置邏輯應該位於WCF Services的Global.asax檔案中,以便在WCF Service Application啟動的時候,所有組件都能成功地初始化。我將在今後的CQRS案例中進一步描述這一點。
至此,Tiny Library的服務端部分基本介紹完畢,回顧一下,這些內容包括:領域建模、倉儲實現和應用服務層。下一講將簡單介紹一下Tiny Library的Web介面的設計與開發。由於本系列文章的重點不是討論某個技術的具體實現,因此,在下一講中不會涉及太多有關ASP.NET MVC的細節內容。