您好,欢迎访问三七文档
当前位置:首页 > 电子/通信 > 综合/其它 > 在ASPNETMVC4中使用领域驱动设计
在领域驱动设计中,我们首先会为领域设计模型,且在设计的过程中完全不考虑存储和具体的实现细节。也就是说在设计中,我们把数据库放在了一个次要的位置(其实仍然很重要,只是在设计架构时我们不考虑它)。有了模型以后,下一步就是如何持久化(保存数据),且这里就存在着面向对象方式表示的数据和数据库所使用的基于元组的关系模型之间的不一致(对象-关系阻抗失谐)。如果可以选用面向对象数据库,事情就会容易许多,然而面向对象数据库并没有成为IT界的主流,因此关系型数据库就成为你唯一的选择。之所以会出现基于领域的设计是为了处理真实世界中的复杂性。运用领域驱动设计之后,你要么选用面向对像数据库,要么引入一个O/R映射层。然而实现领域驱动设计很复杂,甚至可以说以个人之力完全无法实现。幸好现在有很多框架可用,例如EntityFramework(以下简称EF)。使用EF有三种方法:1.数据库优先,即先设计数据库,然后做数据库和面向对象模型的映射;2.模型优先,先用EF自带的设计工具设计模型,然后生成sql脚本,创建数据库;3.代码优先,先使用其他UML工具设计模型,根据模型手动编写模型代码,然后生成sql脚本,创建数据库。目前最流行的是代码优先,因为它最灵活,然而代码优先的工作量很大。而数据库优先的方法和领域驱动设计的思想相悖。所以我选择了模型优先的方法,但在EF4中,使用模型优先是有问题的,它生成的模型都继承自一个名为EntityObject的基类,这个基类包含了访问数据库的方法,这就不支持持久化透明了。幸好在EF4.1以后微软引入了DbContext,用EF4.1的模型优先可以生成支持持久化透明的实体类,不继承任何基类,数据访问由DbContext实现。在VS2012中创建一个ASP.NETMVC4项目,VS2012会自动添加EF5.0的引用,并且在设计模型时会自动生成支持持久化透明的实体类,但在VS2010中要麻烦点,这里不做说明了,因为用了VS2012就完全没有问题了。具体建模过程不做介绍,网上一搜一大堆。下图为模型的一部分:在传统的三层架构设计中,三层架构分为数据访问层、业务逻辑层和表现层。很多人在自己实现三层架构的时候都会明确的定义DAL、BLL和UI,但在实现的过程中总会有一些误解,例如很多人误把用户操作的流程当成业务逻辑,而数据访问层就是大量的sql语句和连接数据库的操作。实际上,用户的操作流程应该称为应用逻辑,应该放在服务层。我看过很多人设计的三层架构,BLL层几乎没有什么代码,只是简单的对DAL层的调用,复杂的数据访问逻辑都在DAL层。可以说,这种三层架构并不是真正意义上的三层架构,只是看起来像三层架构。其实三层架构是一种很复杂的架构模式,个人之力很难实现。在领域驱动设计中,业务逻辑层主要关注于领域的对像模型,每个业务对象都有其数据和行为,即业务逻辑层表示的是领域对象之间的关系逻辑,在你建模完成以后业务逻辑层就已经设计完成了,即使你一行代码都没写。例如上图的模型图,可以很直观的看出来各模型之间的关系,当我们需要时可以从一个模型很容易的导航到另一个模型,然后由EF的DbContext访问数据库,而不需要编写任何sql语句。这样的设计并没有明确定义DAL、BLL,在项目中完全看不到这样的类或类库,这里的三层只是逻辑上的三层。DbContext负责数据访问,领域模型包含业务逻辑。但是在一般情况下,不应该在UI层直接使用DbContext访问数据库,而是要先定义一个仓储层(repository),这是领域驱动设计中最常用的做法,首先定义一个仓储接口,包含CRUD操作。如下图:然后再定义一个Repository类实现这个接口:这个类需要一个泛型参数,该参数的基类是DbContext,在构造方法里直接创建它。构造方法还需要一个IErrorLogger类型的参数,用于记录访问数据库时出现的异常,通过依赖注入(DependencyInjection,DI)的方式传入(控制反转(IoC)原则,后面还将重点介绍。)表现层使用的是ASP.NETMVC4,这才是本文的重点。新建一个MVC4项目(过程不做介绍),然后新建一个名为ArticleController的控制器,在ArticleController控制器里新建一个名为Articles的控制器操作(action),用来处理用户对文章列表的请求:这个控制器依赖于IRepository接口,通过IRepository访问数据库。这里似乎有个问题,是不是UI层直接调用数据访问层了呢?其实不是,这里的控制器仅依赖于IRepository接口,并非数据访问层,数据访问层由EF实现,而非我们自己实现。当然这里的设计还有点瑕疵,稍后会做改进。下面的问题就是,这个控制器依赖于IRepository接口,那么我们应该在哪里初始化这个接口呢?根据上面在Repository构造方法参数中传入IErrorLogger的经验,我们也可以在控制器的构造方法中传入一个IRepository类型的实例。这是行得通的,并且构造方法参数注入是最常用的依赖注入方法。下面有必要介绍一下依赖注入。在介绍依赖注入之前,需要先介绍一下依赖倒置原则(DIP),作为一条设计原则,依赖倒置原则强调高层组件应该依赖于抽象而不是某个具体的实现或功能。例如上面的两个例子,Repository类不依赖于IErrorLogger的具体实现,仅依赖于IErrorLogger这个接口,我们只知道这个接口有一个记录日志的功能,但并不知道它具体是怎么实现的(IErrorLogger是个抽象的概念),只有当我们需要某个记录日志的功能时,才会把实现了IErrorLogger接口的类的实例传递给Repository。ArticleController同样只依赖于IRepository这个抽象接口。控制反转(IoC)就是依赖倒置原则的一个应用,用一段泛化的代码控制更加特定的外部组件的执行。在这里的讨论中,控制反转和依赖注入可以认为是同义词。不过在字面上二者并不总是同义词,控制反转有时会指原则,而依赖注入则代表了原则的应用—即模式。实际上,控制反转历史上曾是基于依赖倒置原则的一个模式。依赖注入这个名词由MartinFowler提出(【企业应用架构模式】),用来专指控制反转的概念。控制反转/依赖注入(IoC/DI)作为一种模式,可以让高层次方法不再需要辅助组件的具体引用。注入的过程可以通过3种方式实现,一种是通过被注入方法所在的类的构造方法,前面两个例子都是采用了这种方法;二是通过在被注入方法所在的类中定义一个方法或属性;最后一种是让被注入方法所在的类实现一个接口,接口中提供了辅助组件的具体实现。IoC/DI的目的就是降低组件之间的耦合度(高内聚,低耦合。)现在IoC/DI通常会有专门的框架提供,这些框架也提供了很多高级功能,下面还会介绍。介绍完了依赖注入,下面继续看这个例子,虽然在ArticleController的构造方法中定义了传入IRepository的方法,但是直到现在还没有在任何地方创建一个实现了IRepository的实例,也就是说直到现在我们看到的IRepository仍然是抽象的。虽然我们已经知道Repository这个类实现了IRepository接口,并且显而易见我们在控制器中肯定会用到Repository这个类,但是究竟要在哪里创建这个类的实例呢?直接在控制器中创建?那控制器就直接依赖于Repository了,违反依赖倒置原则。这里就要用到一个IoC/DI框架了—Ninject,看起来像个日语词汇,没错,它有忍者的意思,快如闪电!首先需要在项目中安装Ninject,使用NuGet安装,在NuGet命令行中输入:install-packageNinject.MVC3(没错,是MVC3,虽然我用的是MVC4,但这个包同样支持MVC4。)安装完成以后会在App_Start文件夹中生成一个NinjectWebCommon.cs文件,打开这个文件。这里的代码会在应用程序启动之前执行,下面为我们的IRepository接口注册一个IoC容器,在RegisterServices方法中添加代码:这段代码的作用就是为IRepository注册一个具体实例RepositoryMainContext,MainContext是建模时定义的一个上下文,继承自DbContext,同时会为Repository传入一个名为LogErrorToFile的错误日志实例(LogErrorToFile实现了IErrorLogger接口中用于记录日志的方法),用于把错误信息记录到文本中(完全可以重新定义一个LogErrorToDB用于把错误信息记录到数据库,这就是依赖注入的好处,可以根据需要定义任何记录错误日志的方法,而不需要修改Repository的任何代码,即Repository不直接依赖于任何记录日志的具体实现。)当用户向ArticleController发送请求时,Ninject会通过ArticleController的构造方法传入一个IRepository的具体实现(这里是RepositoryMainContext),从而控制器间接依赖于RepositoryMainContext,如果有必要,将来完全可以重新定义一个IRepository的实现,例如不使用EF的数据访问层(NHibernate?),而不需要修改控制器的任何代码,降低了耦合度,提高了代码的复用性。前面说了,控制器中的设计还有一点瑕疵,现在来改进一下。上面Articles中的请求逻辑非常简单,从数据库中读取前10条文章,如果有一个很复杂的请求逻辑,可能这里就要写很多代码,那么控制器中的代码就会很长。并且这个请求逻辑可能还会出现在其他地方,那时候就又要写一段重复的代码。所以我们可以考虑把这段代码提出来,单独放到一个类中,这个类就是服务类:这个服务类同样依赖于IRepository接口,并且同样使用了构造方法参数注入的方式传入一个IRepository的实例。服务层同样不直接依赖于特定的仓储层。那么控制器中的代码就可以修改成这样:但是追求完美的人可能又会提出一个问题,这里的控制器虽然不直接依赖于特定的仓储层,但是却直接依赖于ArticleService这个类,是不是违反了低耦合的原则呢?确实是这样,这里是采取了一个折中方案。因为服务层是对表现层应用逻辑的封装,服务层的作用就是向表现层暴露模型对象数据,可以认为服务层是表现层的一部分。当然我们也可以定义一个IArticleService接口,让控制器依赖于这个接口,而不是IRepository,那么在Ninject中也就不为IRepository注册实例,而是为IArticleService注册:IRepositoryrepository=newRepositoryMainContext(newErrorLogger.LogErrorToFile());kernel.BindIArticleService().ToArticleService().WithConstructorArgument(repository,repository);这同样是不错的设计,如果有一个请求用户数据的控制器UserController,则同样需要依赖于一个IUserService接口,那么也需要在Ninject中注册:kernel.BindIUserService().ToUserService().WithConstructorArgument(repository,repository);在应用程序中会针对每一个模型建立一个访问该模型的服务,就会产生大量的服务,那么在应用程序启动之前,就需要为每个服务注册一个IoC容器,但是服务的数量再多也是有限的,这并不会影响应用程序的效率(几十几百个服务都不是大问题),因此这种设计要更优秀一点。但是这样代码量就会变大,至于偏向哪一种,完全看个人爱好,如果追求完美可以考虑这种设计。可能会有人提出来,难道不能定义一个更高级
本文标题:在ASPNETMVC4中使用领域驱动设计
链接地址:https://www.777doc.com/doc-2561478 .html