领域驱动设计案例:Tiny Library:应用服务层

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的理解还是有困难,我在此将我的理解写下来,供大家参考

  1. 领域模型对象不负责任何数据传输的功能,这是因为,如果将领域模型对象用于数据传输,则势必需要在另一个层面(或者另一个进程中)产生一个领域模型对象的副本,此时才有可能在服务器与客户机之间产生一种数据契约,而这种做法违背了层内高内聚,层间低耦合的原则
  2. DTO是简单而原始的,并且是运行时上下文无关的。这是为了满足序列化/反序列化的需求。因此,DTO所包含的数据要不就是原始数据类型,要不就是其它的DTO
  3. 客户端需求决定DTO的设计。因此,DTO与领域模型对象并非一一对应的关系,相反,它是为了满足客户端需求而存在的

在Tiny Library案例中,我选用了WCF的Data Contracts作为DTO的实现标准,因为这样做不仅能够基于客户端需求设计合理的DTO结构,而且还可以利用WCF的DataContractSerializer实现DTO的序列化/反序列化。不仅如此,WCF为我们在服务器与客户机通讯上提供了技术支持和有力保障。

打开TinyLibrary.Services项目,我们可以看到一个IDataObject的泛型接口,其定义如下:

隐藏行号 复制代码 IDataObject定义
  1. public interface IDataObject<TEntity> where TEntity : IEntity
    
  2. {
    
  3.     void FromEntity(TEntity entity);
    
  4.     TEntity ToEntity();
    
  5. }
    
  6. 
    

我们可以要求所有的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中“还书”操作的具体实现
  1. public void Return(string readerUserName, Guid bookId)
    
  2. {
    
  3.     try
    
  4.     {
    
  5.         using (IRepositoryTransactionContext ctx = ObjectContainer
    
  6.             .Instance
    
  7.             .GetService<IRepositoryTransactionContext>())
    
  8.         {
    
  9.             IRepository<Book> bookRepository = ctx.GetRepository<Book>();
    
  10.             IRepository<Reader> readerRepository = ctx.GetRepository<Reader>();
    
  11.             Reader reader = readerRepository.Find(Specification<Reader>.Eval(r => r.UserName.Equals(readerUserName)));
    
  12.             Book book = bookRepository.GetByKey(bookId);
    
  13.             reader.Return(book);
    
  14.             ctx.Commit();
    
  15.         }
    
  16.     }
    
  17.     catch
    
  18.     {
    
  19.         throw;
    
  20.     }
    
  21. }
    
  22. 
    

上面的代码展示了“还书”操作的具体实现方式,我们可以看到,位于应用层的WCF Services仅仅是协调仓储操作和事务处理,业务逻辑由reader.Return方法实现:

隐藏行号 复制代码 TinyLibrary.Domain.Reader的Return方法
  1. public void Return(Book book)
    
  2. {
    
  3.     if (!book.Lent)
    
  4.         throw new InvalidOperationException("The book has not been lent.");
    
  5.     var q = from r in this.Registrations
    
  6.             where r.Book.Id.Equals(book.Id) &&
    
  7.             r.RegistrationStatus == RegistrationStatus.Normal
    
  8.             select r;
    
  9.     if (q.Count() > 0)
    
  10.     {
    
  11.         var reg = q.First();
    
  12.         if (reg.Expired)
    
  13.         {
    
  14.             // TODO: Reader should pay for the expiration.
    
  15.         }
    
  16.         reg.ReturnDate = DateTime.Now;
    
  17.         reg.RegistrationStatus = RegistrationStatus.Returned;
    
  18.         book.Lent = false;
    
  19.     }
    
  20.     else
    
  21.         throw new InvalidOperationException(string.Format("Reader {0} didn't borrow this book.",
    
  22.             this.Name));
    
  23. }
    
  24. 
    

 

配置文件

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的细节内容。

posted @ 2010-11-02 08:11  dax.net  阅读(8081)  评论(11编辑  收藏  举报