求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
域驱动设计案例:Tiny Library
 

作者:陈晴阳,发布于2013-1-11,来源:CSDN

 

目录:

域驱动设计案例:Tiny Library:简介

领域驱动设计案例:Tiny Library:业务逻辑与系统结构

领域驱动设计案例:Tiny Library:领域模型

领域驱动设计案例:Tiny Library:仓储

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

领域驱动设计案例:Tiny Library:用户界面

领域驱动设计案例:Tiny Library:简介

应广大网友的要求,我最近抽空基于ASP.NET MVC + WCF + Entity Framework做了一个案例,该案例以图书馆图书管理、读者借书、还书为业务背景,以领域驱动设计为思想指导,全程采用Microsoft技术进行实践,希望能够给Microsoft技术的狂热者以及领域驱动设计的学者提供实践参考。

本案例选用的业务逻辑非常简单,所以项目取名上我选用了“Tiny Library”,在后面一章我将详细介绍这个案例的业务逻辑、模型设计与系统架构。

下载案例

本来打算将项目发布到codeplex上,便于大家交流,也便于代码更新与维护,但由于某些原因,我无法在自己的网络环境中连接codeplex的svn/tfs服务,于是,目前只能以压缩包的形式发布案例源代码,希望大家谅解,等以后有机会更新到codeplex上后再通知大家。

【请单击此处下载案例源代码】

系统需求

  • Microsoft Visual Studio 2010
  • Microsoft Patterns & Practices 5.0(v5.0.414.0,Runtime v2.0.50727。请自行到Microsoft官方网站下载安装)
  • Microsoft ASP.NET MVC 2
  • Microsoft Entity Framework(注意:是Visual Studio 2010自带的那个版本,而不是最新发布的那个Feature Pack CTP版本)
  • Microsoft SQL Express 2008 SP1
  • Apworks Application Development Framework

请在打开本案例解决方案之前自行安装上述软件和组件!

说明:Apworks Application Development Framework是我自己开发的一套领域驱动(Domain Driven)的应用程序开发框架,里面提供了对Aggregate Root、Repositories、Specifications以及Transaction Context的支持,基本能够满足基于Microsoft.NET技术的中小型领域驱动项目的应用开发。目前这个框架项目正在进一步实现基于CQRS体系结构模式的框架。为了节约时间,本系列文章不会对Apworks Application Development Framework做太多介绍。本框架目前也还是under construction,所以读者朋友也千万不要将其用在自己的系统开发中,以免发生危险!有关Apworks Application Development Framework的源代码以及更多信息,请访问项目站点:http://apworks.codeplex.com。Tiny Library压缩包里包含了一个可被Tiny Library使用的Apworks版本,因此读者朋友无需自己去Apworks站点上下载并编译源代码。当然,如果您希望了解Apworks的实现方式,可以使用上面的站点查看Apworks的源代码。

安装部署

1.建立数据库

使用Microsoft Visual Studio 2010提供的Server Explorer功能,在Data Connections上单击鼠标右键,选择Create New SQL Server Database选项,此时出现Create New SQL Server Database对话框,在对话框的Server name中输入(local)\SQLEXPRESS,在New database name中输入TinyLibraryDB,之后单击OK按钮

2.创建数据库Schema

使用Microsoft Visual Studio 2010打开TinyLibrary解决方案,在TinyLibrary.Domain项目节点下找到TinyLibrary.edmx.sql脚本文件,打开此脚本文件,在SQL Editor区域,点击鼠标右键,选择Connection | Connect菜单,此时弹出Connect to Database Engine对话框,Server选择SQLEXPRESS,然后单击OK

再次在SQL Editor区域点击鼠标右键,选择Execute SQL选项,执行SQL脚本以创建数据库Schema

3.建立演示数据(Demo Data)

以上述同样的方式,打开TinyLibrary.Domain项目下的TinyLibrary.DemoData.sql脚本并执行

4.3722端口

Tiny Library的WCF Service采用3722端口作为其服务的固定端口,因此在使用本案例钱,确保该端口未被其它应用程序占用

运行案例

1.在Microsoft Visual Studio 2010的Solution Explorer上,右键单击TinyLibrary Solution然后选择Rebuild Solution以重新编译解决方案

2.在TinyLibrary.Services项目下,选中TinyLibraryService.svc,然后单击右键,选择View in Browser,此时会自动打开ASP.NET Development Server,端口占用3722,同时打开WCF Service的页面。此时将WCF Service的页面关闭,仅留下ASP.NET Development Server

3.右键单击TinyLibrary.WebApp项目,选择Set as StartUp Project选项,然后在Microsoft Visual Studio中按下Ctrl+F5或者Debug | Start Without Debugging选项以启动应用程序

4.应用程序启动后,可以看到主界面如下

登录账号

测试需要,Tiny Library默认提供三个用户账户:daxnet、acqy和james。用户名、密码如下:

1.登录名:daxnet;名称:DaxNet;密码:daxnet@live.com

2.登录名:acqy;名称:Sunny Chen;密码:acqy@163.com

3.登录名:james;名称:james;密码:james@tinylibrary.com

额外说明

时间有限,本案例仅仅是一个基于Microsoft.NET技术的领域驱动设计实践案例,因此,如下内容没有包含在本案例中:

1.基于AOP和Policy Injection的技术实践。这包括:异常处理、数据验证与系统日志

2.基于用户/角色验证的图书维护页面

3.ASP.NET MVC的高级应用

4.WCF的异常捕获与显示

5.单元测试

6.其它的一些技术细节

有兴趣的朋友可以在本案例源代码的基础上进行扩充,以实现一套完整的图书馆管理应用。

领域驱动设计案例:Tiny Library:业务逻辑与系统结构

之前我发布了领域驱动设计的一个实践案例:Tiny Library。本章介绍该案例实现的业务逻辑与系统结构设计。

业务逻辑

Tiny Library的业务逻辑非常简单,主要就是如下两条:

1.任何用户可以添加Library中的图书(简化起见,图书不能修改也不能删除),也可以查看图书的详细信息

2.注册用户,也就是读者,可以借书、还书、查看自己借过的图书列表和借书信息

篇幅有限,我就不在此将案例的操作过程一一截图了,读者朋友们可以自己下载源代码,然后在Visual Studio 2010中编译运行。

系统结构

由于是领域驱动设计,本案例系统分层与传统分层略有不同。分为四层:展现层、应用服务层、领域层和基础结构层。展现层采用ASP.NET MVC框架实现;应用服务层则是一个WCF Service;领域层采用Entity Framework结合本人自己研发的Apworks Application Development Framework;基础结构层则为整个应用提供了IoC、Caching、Specifications、Repository等的具体实现。整个系统架构基本上可以以下图描述:

需要说明的是,在上图中的Domain Model和EdmRepository之间出现了双向依赖,于是我在边上用黄色注释文字作了注解。其实,Domain Model只依赖于仓储(Repository)的接口,而EdmRepository是基于Entity Framework的一种仓储实现方式,它实现IRepository接口,同时也对Domain Model产生依赖,以获得对聚合根的访问。关键的一步在于,Tiny Library采用依赖注入,将EdmRepository注射到Domain Model中,于是,Domain Model根本不依赖于仓储的具体实现方式,保证了领域模型层面的纯净度。领域驱动设计的社区中有很多朋友都对这个问题产生疑问,Tiny Library就是一个回答该问题的很好的案例。

Visual Studio 2010解决方案结构

Tiny Library在Microsoft Visual Studio 2010的解决方案下包含六个项目:TinyLibrary.Design、TinyLibrary.Domain、TinyLibrary.Repositories、TinyLibrary.Services、TinyLibrary.WebApp以及TinyLibrary.WebApp.Tests。如下所示:

1.TinyLibrary.Design:项目的一些设计图稿,其中包括上面的架构图。一般没什么用(注意:第一个版本的Tiny Library解决方案中不包含这个项目,不过没关系,不影响大家的阅读和学习)

2.TinyLibrary.Domain:领域模型项目,其中包括了Tiny Library的领域模型与业务逻辑,也是本案例的核心所在。为了能够以可视化的方式设计领域模型,也是为了采用Microsoft技术来实现案例,本项目采用了Microsoft Entity Framework作为建模工具

3.TinyLibrary.Repositories:仓储的具体实现项目,它引用TinyLibrary.Domain项目,同时引用Apworks组件以实现IRepository接口。本项目仅包含了针对Entity Framework的仓储实现,同时也一并实现了Repository Transaction Context对象

4.TinyLibrary.Services:WCF Application Service项目,它占用3722端口,其目的是与Presentation层交互。交互采用Data Transferring Objects(DTO),实现上是一组位于DataObjects目录下的Data Contracts。从这个项目可以看到,DTO与Entity/AggregateRoot并非是一一对应的,虽说在Tiny Library中,看上去是一个Entity/AggregateRoot对应一个Data Object,但仔细阅读可以发现,这些Data Objects中包含的数据跟与之对应的Entity/AggregateRoot中包含的对象状态是有出入的。这是根据应用程序的需求来决定的

5.TinyLibrary.WebApp:本案例的主程序:一个ASP.NET Web应用程序,以ASP.NET MVC框架为基础,提供用户界面与交互接口

6.TinyLibrary.WebApp.Tests:TinyLibrary.WebApp项目的单体测试项目,在本案例中,它是创建ASP.NET MVC项目时自动生成的,我没有去写或者修改任何Test Case,所以,可以直接无视这个项目

从下一讲开始,我将详细介绍Tiny Library的实现过程。这个过程不会是一步步的手把手教如何创建项目、如何编码等,而是对领域驱动设计在Microsoft .NET技术下的实践进行介绍。这些内容将包括:领域模型的实现、仓储的设计与实现、WCF Service与DTO、ASP.NET MVC的整合、领域驱动设计案例答疑以及Apworks Application Development Framework简介等。

领域驱动设计案例:Tiny Library:领域模型

本讲主要介绍基于Entity Framework的领域驱动设计建模。首先回顾一下Tiny Library的业务逻辑:

1.任何用户可以添加Library中的图书(简化起见,图书不能修改也不能删除),也可以查看图书的详细信息

2.注册用户,也就是读者,可以借书、还书、查看自己借过的图书列表和借书信息

请注意上面描述的黑体部分,这些概念出现在Tiny Library的领域知识(Domain Knowledge)中,换言之,是Tiny Library领域的通用语言的组成元素。

一、实体与聚合根

首先分析出实体,不难看出,读者和图书是实体;由于每个读者都将有自己的借书信息(比如,什么时候借的哪本书,是否已经归还,或者是否已经过期),而与之对应地,每本书也可以有被借历史(比如,这本书是什么时候借给哪个读者),于是,借书信息也是实体。

再来看看聚合。借书信息是与读者和图书关联的,也就是说,没有读者,借书信息没有存在的意义,同样,没有图书,借书信息也同样不存在。每个读者可以没有任何借书信息(或者说借书记录),也可以有多条借书信息;而每本书也同样可以没有任何被借信息(或者说被借记录),也可以有多条被借记录。因此存在两个聚合:读者-借书信息聚合(1..0.*)以及图书-借书信息聚合(1..0.*)。读者和图书分别为聚合根,借书信息为实体。与Tiny Library对应起来,总结如下:

  • 读者:Reader,聚合根
  • 图书:Book,聚合根
  • 借书信息:Registration,实体

根据上述描述,我们可以确定,我们将来需要针对读者(Reader)和图书(Book)实现仓储以及相应的规约。

二、基于Entity Framework建立领域模型

目前Entity Framework支持三种建模方式:Model First、Database First以及Code First。Code First是在今年刚发布的Feature Pack中才支持的。为了迎合领域驱动设计思想,我们采用Model First。

根据上面的分析,现建模如下:

注意:如何在Visual Studio中使用Entity Framework进行Model First建模不是本文讨论的重点,读者朋友请自己参阅相关文档。

此时,我们需要使用C#部分类的特性,将Reader和Book定义为聚合根,将Registration定义为实体。我开发的一个DDD框架(Apworks)中为聚合根和实体的接口作了定义,现在,只需要引用Apworks的程序集,然后使用部分类的特性,让Reader和Book实现IAggregateRoot接口,让Registration实现IEntity接口即可。从技术上看,这样就将Apworks框架整合到了领域模型中。代码如下:

  public partial class Reader : IAggregateRoot
   {
   }

  public partial class Book : IAggregateRoot
   {
   }

  public partial class Registration : IEntity
   {
   }

三、添加业务逻辑

根据DDD,实体是能够处理业务逻辑的,应该尽量将业务体现在实体上;如果某些业务牵涉到多个实体,无法将其归结到某个实体的话,就需要引入领域服务(Domain Service)。Tiny Library案例业务简单,目前不会涉及到领域服务,因此,在本案例中,业务逻辑都是在实体上处理的。

以读者(Reader)为例,它有借书和还书的行为,我们将这两种行为实现如下:

  public partial class Reader : IAggregateRoot
 {
     public void Borrow(Book book)
     {
         if (book.Lent)
             throw new InvalidOperationException("The book has been lent.");
         Registration reg = new Registration();
         reg.RegistrationStatus = RegistrationStatus.Normal;
         reg.Book = book;
         reg.Date = DateTime.Now;
         reg.DueDate = reg.Date.AddDays(90);
         reg.ReturnDate = DateTime.MaxValue;
         book.Registrations.Add(reg);
         book.Lent = true;
         this.Registrations.Add(reg);
     }

    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));
     }
 }

业务逻辑的添加仍然是在我们新建的partial class中,这样做的目的就是为了不让Entity Framework的代码自动生成器覆盖我们手动添加的代码。相应地,我们在Book和Registration中实现各自的业务逻辑(具体请参见案例源代码)。从TinyLibrary.Domain这个Project上看,TinyLibrary.edmx定义了基于Entity Framework的领域模型,而其它的几个C#代码文件则使用部分类的特性,分别针对每个实体/聚合根实现了一些业务逻辑。

下一讲将详细介绍基于TinyLibrary领域模型与Entity Framework的仓储的实现方式。

领域驱动设计案例:Tiny Library:仓储

在领域驱动设计的案例中,仓储的设计是很具有争议性的话题,因为仓储这个角色本身就与领域模型和基础结构层对象相关,它需要序列化领域对象(应该说是聚合),然后将其保存到基础结构层的持久化机制。于是,在领域驱动设计的社区中,存在两种观点:

1、领域模型不能访问仓储,理由是:仓储需要跟技术架构层打交道,在领域模型中访问仓储就会破坏领域模型的纯净度。需要使用仓储的,需要在领域模型上加上一层,比如Application层,在该层中获取仓储实例并处理持久化逻辑

2、领域模型可以访问仓储,但仅仅是通过仓储接口和IoC容器访问仓储;仓储的具体实现通过IoC注入到领域模型中

其实,这只不过是个人习惯问题,我认为两种方法都可以接受,具体采用哪种方法,就要看具体项目的需求和实现情况而定。在Tiny Library中,由于业务简单,所以采取的是第一种方式,但上述的理由并不充分,换句话说,出于业务需求,我采用了第一种方式,但并不是因为仓储需要跟技术架构层打交道所以才把对仓储的访问放在Application层中。仓储也是领域模型的一部分,领域模型依赖于仓储的抽象。

在TinyLibrary中,仓储的实现仍然依赖Apworks Application Development Framework,因为Apworks已经为仓储、规约的实现搭好了架子,我们无需重新编写一套仓储的实现代码。

一、EdmRepository<TEntity>基类

此类继承于Apworks.Domain.Repositories.Repository<TEntity>类,它的主要任务是定义一个Entity Framework中的Object Context(在本案例中为TinyLibraryContainer),然后通过只写属性的方式,向Repository Transaction Context提供一个设置接口;另外,EdmRepository还实现了几个抽象方法,这样做是为了便于子类的实现(开发人员可以少写点重复代码)。

二、ReaderRepository和BookRepository子类

这两个类提供了基于Reader聚合和Book聚合的仓储实现。泛型约束分别绑定到Reader类和Book类(详情请参考源代码)。

三、EdmTransactionContext类

此类实现Apworks.Domain.Repositories.IRepositoryTransactionContext接口,目的是基于Entity Framework实现一个事务处理上下文。仓储将由此上下文创建获得。从此类的GetRepository泛型方法可以看到,仓储的实体是通过IoC容器获得的,IoC容器的配置位于TinyLibrary.Services项目的web.config文件中。

整个Repository的实现类图如下:

四、TinyLibrary.Services项目app.config配置文件

打开TinyLibrary.Services项目的app.config配置文件,我们可以看到,Apworks采用Unity作为IoC容器:

<apworksConfiguration>
  <objectContainer provider="Apworks.IoC.Unity.UnityContainer, Apworks.IoC.Unity"/>
  <modelAssemblies>
    <modelAssembly name="TinyLibrary.DomainModel"/>
  </modelAssemblies>
  <caching>
    <cached key="Infrastructure.ObjectContainer" active="true" cacheManager="cacheManager"/>
    <cached key="Domain.Repository" active="true" cacheManager="cacheManager"/>
  </caching>
</apworksConfiguration>  

而Unity的配置部分,我们分别将ReaderRepository和BookRepository注册到了IRepository<Reader>和IRepository<Book>接口上:

<unity>
  <containers>
    <container>
      <types>
        <type type="Apworks.Domain.Repositories.IRepositoryTransactionContext, Apworks.Domain"
              mapTo="TinyLibrary.Repositories.EdmTransactionContext, TinyLibrary.Repositories">
        </type>
 
        <type type="Apworks.Domain.Repositories.IRepository`1[[TinyLibrary.Domain.Book, TinyLibrary.Domain]],
 Apworks.Domain"  mapTo="TinyLibrary.Repositories.BookRepository, TinyLibrary.Repositories"/>
        
        <type type="Apworks.Domain.Repositories.IRepository`1[[TinyLibrary.Domain.Reader, TinyLibrary.Domain]],
 Apworks.Domain" mapTo="TinyLibrary.Repositories.ReaderRepository, TinyLibrary.Repositories"/>
 
        <type type="Apworks.Core.IIdentityGenerator, Apworks.Core"
              mapTo="Apworks.Core.GeneralGuidGenerator, Apworks.Core"/>
      </types>
    </container>
  </containers>
</unity> 

五、关于项目依赖(Project Dependencies)

细心的读者会发现,TinyLibrary.Repositories引用了TinyLibrary.Domain项目,这是为了实现仓储的本职工作:持久化领域模型中的聚合。假设领域模型需要访问仓储,则无法直接引用TinyLibrary.Repositories,只能通过IoC/DI。事实上,TinyLibrary.Services项目根本没有去引用TinyLibrary.Repositories项目,因为TinyLibrary.Services并不需要去依赖仓储的具体实现方式,于是从逻辑上讲,不应该将TinyLibrary.Repositories项目添加到TinyLibrary.Services的引用中去。然而,TinyLibrary.Services的正常运行,是需要TinyLibrary.Repositories的支持的,这个在配置文件中已经体现出来了。为了开发方便,我对这些项目作了如下改动:

1.右键单击TinyLibrary.Repositories项目,选择Properties,在Build选项卡下的Output部分,将项目的编译输出指定到TinyLibrary.Services的bin目录下

2.右键单击TinyLibrary.Services项目,选择Project Dependencies,在弹出的对话框中,勾选TinyLibrary.Repositories项目

早在《EntityFramework之领域驱动设计实践(八)》一文中,我就介绍了仓储的实现方式,在那篇文章的最后给出了一幅图,描述了各个组件之间的关系,这种关系在Tiny Library的项目中也是适用的,在此再转贴一次,以示总结。

领域驱动设计案例: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的泛型接口,其定义如下:

   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充当了应用层的职责,它不负责处理任何业务逻辑,而是从更高的层面,为业务逻辑的正确执行提供适当的运行环境,同时起到任务协调的作用(比如事务处理和基础结构层服务调用)。

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方法实现:

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

领域驱动设计案例:Tiny Library:用户界面

工作繁忙,很久没有更新博客了。下面言归正传,简单介绍一下Tiny Library的用户界面实现。

如前所述,Tiny Library采用ASP.NET MVC框架实现了基于浏览器的用户界面。回顾一下《业务逻辑与系统结构》一文,从层次架构图中我们可以清楚地看到,用户界面层是通过WCF Services与系统交互的。

为了快速简单地实现Tiny Library,我只是简单地使用了ASP.NET MVC,因此没有对其做更深入的挖掘,有关ASP.NET MVC的更多知识,请朋友们自行上网搜索或阅读相关书籍。

这里需要说明的问题有两点:用户认证与授权,以及MVC中的M。

用户认证与授权(Authentication & Authorization)

ASP.NET MVC采用基于Forms的认证机制(Forms Authentication),因此Tiny Library也继承了这种方式。事实上,使用ASP.NET MVC的这种认证机制,会在站点的App_Data目录下产生一个ASPNETDB.MDF的数据库文件,用来保存Authentication相关信息,比如某个用户注册了,就会在这个数据库中产生相关信息。然而,我们的Tiny Library也有一套自己的数据库,该库中需要保存用户的相关信息(比如Reader的名字等),目前我是在AccountController中,Register用户的时候,同时在Tiny Library中同步一条数据,代码如下:

MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email);
if (createStatus == MembershipCreateStatus.Success)
{
    TinyLibraryServiceClient svcClient = new TinyLibraryServiceClient();
    bool ret = svcClient.AddReader(new ReaderData
    {
        Id = Guid.NewGuid(),
        Name = model.Name,
        UserName = model.UserName
    });
    svcClient.Close();
    if (ret)
    {
        FormsService.SignIn(model.UserName, false /* createPersistentCookie */);
        return RedirectToAction("Index", "Home");
    }
    else
    {
 
    }
}
else
{
    ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
}

这样做其实很不保险,比如,如果在Tiny Library数据库中添加用户信息失败,那么虽然用户成功注册(提交ASPNETDB.MDF成功),也无法正常使用系统。当然,Tiny Library只是个演示系统,如果是在实际应用中,这个问题还需要通过其他途径解决。我目前没有想到万全之策,欢迎热心的朋友在本文的留言中给出建议。

MVC中的M(Model)

有不少使用ASP.NET MVC的朋友,总会将Domain Model带到用户界面层,然后直接将Domain Model中的实体用作MVC中的M。在Fowler的PoEAA一书的DTO一章已经说过,Domain Model是不能暴露给高层的,因此将Domain Model中的实体用于MVC是不合理的。

其实如果你有了解过CQRS体系结构模式,你就会了解到,MVC中的M,应该是Presentation Model,这个Presentation Model与领域毫无关系,仅仅用于界面数据绑定。在MVC Web Application添加了WCF的Service Reference之后,当你创建View的时候,你就可以很方便地将客户端产生的Data Contract用作Data Class,如下:

其实,这里列出来的BookData、ReaderData和RegistrationData,就是Model。

OK,用户界面就写这么些吧,也没有采用什么特别的技术,主要还是使用ASP.NET MVC上的这几个问题。有关ASP.NET MVC的内容,朋友们请自己查阅相关资料吧!


 
分享到
 
 


专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...