自强不息,厚德载物!身心自在,道法自然!


第三话 Asp.Net MVC 3.0【MVC模式】

这一次在此讲述MVC模式,让大家对MVC有一个更加深刻的影响,为大家的深入学习做好坚定的基础!如果对MVC模概念还是混淆的新同学,这话一定要好好学习了!

理解MVC模式

    MVC模式意味着MVC应用程序将被分成至少三个部件:

  • Models(模型):用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。“模型”有对数据直接访问的权力,例如对数据库的访问。“模型”不依赖“视图”和“控制器”,也就是说,模型不关心它会被如何显示或是如何被操作。但是模型中数据的变化一般会通过一种刷新机制被公布。为了实现这种机制,那些用于监视此模型的视图必须事先在此模型上注册,从而,视图可以了解在数据模型上发生的改变。
  • Views(视图):负责呈现数据给客户端,也就是客户端所看到的页面。
  • Controllers(控制器):控制器起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。“事件”包括用户的行为和数据模型上的改变。

由此可以看出MVC的每个模块都是高内聚低偶合,注重点分离,操纵的逻辑数据模型中只包含在模型中,模型逻辑数据仅显示在视图中,代码,处理用户请求和输入只包含在控制器。但是每个模块之间的衔接缺十分微妙,恰到好处。

理解域模型(Domain Model)

             我们创建模型识别的真实世界的实体、操作和规则中存在的行业或活动,我们的应用程序必须支持,称为领域(Domain).对应我们来说,域模型(Domain Model)是一组c#类型(类,结构,等等),被称为域类型(Domain Type).通过定义在领域类型里面的方法表示对领域的各种操作,并且领域的规则也表示在了这些方法里面,当我们创建了一个领域类型(Domain Type)的实例时,也就是创建了一个领域对象(Domain Object),领域模型通常是持久化的,当然持久化有很多方式,通常情况下利用关系型数据库。

Asp.Net MVC的实现

在MVC模式中,控制器是c#类、通常来源于System.Web.Mvc.Controller类,每一个共有的方法我们称为Action Method,这些Action(方法)通过Asp.Net运行时(Runtime)(路由系统)跟可配置的URL相关联。当一个请求发送服务端时Controller(控制器)里的Action(方法)会被相应的执行一些操作域模型(Domain Model),然后选择View(视图)呈现到客户端。如下图1所示:

 

图1

 运用领域驱动开发(Applying Domain-Drivern Development)

域模型是MVC应用程序的一个中心部分,其他的一切包括视图(Views)和控制器(Controllers)都只是一种手段来与之交互Domain Model(领域模型).Asp.Net MVC并不决定技术用于领域模型(Domain)。我们可以自由选择任何技术跟.NET Framework相关的交互,当然这样的技术那就相当多了。但是,Asp.Net MVC的确能为我们的基础设施和约定来帮助域模型(Domain)中的类(Classes)与控制器(Controllers)和视图(Views)连接,当然也包含MVC框架本身。这里有三个关键的特性:

  • 模型绑定(Model bingding):自动使用HTML表单提交的数据来组织领域对象(Domain Objects)基础的约定。
  • 模型元数据(Model metadata):描述了自己写的Model Classes对.Net框架所要表达的意思.Asp.Net MVC能够自动的识别Model Classes显示到Views里面。
  • 验证(Validation):在模型绑定时执行,应用被定义为模型元数据的那些规则。

建立一个简单的域模型(Modeling an Example Domain)

    下面的草图模型为拍卖程序的类图,如下图2.

图2

上面的模型包含了一个Members的集合,每一个Member会有一个Bids集合,每一个Bid对应一个Item,每一个Item可以有多个来自不同Members的Bids实现我们自己的域模型(Domain Model)并作为一个独立的组件其中一个关键的地方是我们选择的语言和术语,这个不是我们的编程语言,而是域建模的通用的语言。

  1. 首先,开发人员倾向于使用编程语言,比如类名,数据库等等名词来表达。而业务专家们是不懂这些的,他们也不需要懂。业务专家知晓一些技术方面的知识是一件非常危险的事情,因为他们会经常根据自己对技术的理解来不断筛选他们的需求,这也就意味着需求会频繁的更改,进而导致开发人员也不知道业务专家的真实需求到底是什么。创建通用语言的方法能够帮助我们避免在一个应用程序里面过度的泛化需求,程序员倾向于建立每一个可能业务实际模型,而不是具体到某一个业务需求。
  2. 在通用语言和域模型(Domain Model)之间的这种连接不应该是非常肤浅的而是向DDD(Domain-Driven Design)专家所建议的那样:对通用语言的任何变化都会导致Model的变化。假如我们让建模跟业务领域不同步,我们就需要建立一种从Model到Domain映射的中间语言,从长远来看,这种做法会导致灾难。为此,我们将创建一个会两种语言的特殊人类,他们随后就开始筛选需求,这却是建立在他们对两种语言都不完全理解的基础之上,当然这样的后果可以想象。

聚合和简化(Aggregates and Simplification)

图2给我提供了一个良好的建立域模型(Domain Model)的开始,但是上面图示的模型并没有提供用C#和SQL Server实现Model的任何有用的帮助,接着就出现许多问题:

  1. 如果我们装载一个Member进入内存,也是不是应该装载Member的Bids以及相关的Items进入内存呢?
  2. 如果我们这样做了,我们是否需要将这个Item其他的bids也加载进内存,并且也将做这些Bids的Members一同加载进内存呢?
  3. 当我们删除一个对象时,我是否应该删除相关的对象呢?如果是,又有哪些呢?
  4. 如果我们选择用文档存储代替关系型数据库来持久化,那哪一个对象的集合应该呈现到同一个文档呢?
  5. .......

所以上面的问题,我们不知道如何解答,我们的域模型也不知道如何解答。

DDD(domain-driven development)的方式回应这些问题是将Domain Objects分配到组里面,这种方式称为聚合(aggregates)。如下图3.

图3.

一个聚合的实体组将若干域对象联合(Together)到了一起,有一个根实体被用来标识整个聚合,它在验证和持久化操作里面充当了"Boss"的角色。在数据变化时,我把聚合当作一个单元来统筹处理,所以我们需要创建呈现在领域模型上下文里面有意义的关系的聚合,并且创建跟实现业务过程一致的逻辑操作。也就是说,我们需要通过分组对象来创建聚合,而这些对象是可以作为一个组被改变的。

DDD(domain-driven development)有一个重要的规则是,在一个聚合实例范围外的对象,只能通过对根实体(Root entity)的引用来持久化,而不是引用在聚合里面的对象。这条规则强化了将聚合里面的对象作为一个单元来对待的概念。在本例子里面,Members和Items都是聚合的根,而Bids只能在作为它们聚合根实体的Item的上下文里面被访问。Bids可以引用Members(根实体),但是Members不能引用Bids(不是根实体)。

着呀好处之一是,它简化了聚合组对象之间的关系的域模型。通常这样能够帮助我们对需要建模的领域的本质的理解。本质上讲,创建聚合约束领域模型和对象之间的关系使得这种关系更加接近于现实领域里面存在的关系。具体C#代码如下:

    public class Member
    {
        public string LoginName { get; set; } 
        public int ReputationPoints { get; set; }
    }
    public class Item 
    {
        public int ItemID { get; private set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime AuctionEndDate { get; set; }
        public IList<Bid> Bids { get; set; }
    }
    public class Bid 
    {
        public Member Member { get; set; }
        public DateTime DatePlaced { get; set; }
        public decimal BidAmount { get; set; }
    }

从上面的代码可以看出,我们很容易就捕捉到了Bids和Members之间的单向关系的本质,当然我们也可以建立一些其他的约束。应用聚合能够帮助我们建立更加有用,更加精确的领域模型,也能够让我们用C#熟练的实现。

一般来说,聚合为一个领域模型增加了结构化和精确化。这也使得应用验证变更方便(根实体变成负责验证状态中的所有对象总体状态),显而易见的单位的持久性。而且,因为聚集在本质上是我们的域的原子单元模型,它们也能够适用事务管理的单元和级联从数据库删除的单元。

另一方面,聚合常常是人为的加上限制。聚合(Aggregates)的概念能够很自然的从文档型数据库得到,但它不是Sqlserver本身的概念,也不是存在大部分ORM工具里的概念.

定义存储库(Defining Repositories)

在某种程度上,我们需要为我们的域模型添加持久性。这通常是通过一个关系或者对象或文档数据库。持久化不是我们领域模型的一部分,它是一个独立的关注点,这也就意味着我们不能将持久化的代码跟定义领域模型的代码混合到一起。通常的方法去执行的分离域模型和持久化系统之间定义存储库。这些都是对象表示的基础数据库。而不是直接处理数据库,域模型调用方法  定义的存储库,这反过来也会使调用到数据库来存储和检索数据的模型.这允许我们分离模型与实现的持久性。这样约定就是为每一个聚合(Aggregates)定义单独的数据模型。在我们的竞拍程序里面,我们可以创建2个Repositories,它们分别是针对Members的Repository和针对Items的Repository。注意这里,我们并不需要创建针对Bids的Repository,因为Bids会作为Items聚会持久化的一部分).代码如下:

 public class MembersRepository
    {
        public void AddMember(Member member) {  }
        public Member FetchByLoginName(string loginName) {  }
        public void SubmitChanges() {  }
    }
    public class ItemsRepository
    {
        public void AddItem(Item item) {  }
        public Item FetchByID(int itemID) {  }
        public IList<Item> ListItems(int pageSize, int pageIndex) {  }
        public void SubmitChanges() { }
    }

需要注意:Repositories仅仅针对加载和保存数据.它们不包含任何其他的逻辑。

 建造松耦合组件(Building Loosely Coupled Components)

 "分离关注点"是MVC模式里面十分非常重要的特性。我们希望组件作为独立的应用程序中,尽可能为我们可以管理带来较少相互依赖关系。 在我们的理想情况下,每个组件都没有任何其他组件直接联系,处理应用程序的其他应用程序领域中只有通过抽象的接口。这就是所谓的松耦合,它使测试和修改我们的应用程序更加容易。比如一个简单的例子:如果我们在编写一个组件,这个组件称为MyEmailSenderto发送电子邮件,我们将实现一个接口,接口定义了所有的邮件发送的功能,我们将这个接口叫IEmailSender,任何其他的应用程序的组件需要引用IEmailSender里面的方法就行了。如下图4(用接口来分离组件)

图4.

通过上图可以看出,我们引入IEmailSender,可以保证PasswordReset和MyEmailSender没有直接的依赖关系。任何实现发邮件的功能都能来代替MyEmailSenderto而不会对PasswordResetHelper造成影响。

注意:不是每一段关系需要解耦使用接口。这个决定我们应用程序是多么复杂,需要什么样的测试,并且需要长期的维护。例如,我们可不去解耦一个小而简单的Asp.Net MVC应用程序。

使用依赖注入(Using Dependency Injection)

什么是依赖注入:简单描述一下;依赖注入其实叫"控制反转"(Inversion of Control,英文缩写为IoC),是一个重要的面向对象编程的法则来削减计算机程序的耦合问题。 控制反转还有一个名字叫做依赖注入(Dependency Injection)。简称DI。应用控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用,传递给它。也可以说,依赖被注入到对象中。所以,控制反转是,关于一个对象如何获取他所依赖的对象的引用,这个责任的反转。它也是一种设计模式!

接口能够帮助我们解耦组件,但是这样仍然面临一个问题,那就是C#并没有提供一种嵌入的方式来比较容易的创建实现接口的对象,我们只能创建一个实现接口的具体实例,比如下面的实现者:

 

public class PasswordResetHelper
{ 
        public void ResetPassword() { 
        IEmailSender mySender = new MyEmailSender(); 
        //...Email Action... 
        mySender.SendEmail(); 
        }
}

 

我们只是做了松耦合的一部分工作,PasswordResetHelper类通过IEmailSender来配置和发送邮件,通过接口的实现来创建对象,这里需要创建一个MyEmailSender的实例。但是我们却让事情更糟,因为现在的PasswordResetHelper同时依赖IEmailSender和MyEmailSender,如下图5.

图5.

其实现在我现在需要一种方式来获取对象(指上面代码里的mySender),这个对象是实现了我们指定的接口但不是去创建实现接口(这里指MyEmailSender)对象本身。对于这样问题的解决方案,我们称为依赖注入(dependency injection(DI)),也可以被认为是控制反转。

DI(dependency injection)是完成或者说是松散耦合的一种模式。DI有分为两个部分:一是删除我们组件里任何对类有依赖的。那么上面我们的例子似乎可以成为下面的样子:

 

public class PasswordResetHelper
{
        private IEmailSender emailSender;
        public PasswordResetHelper(IEmailSender emailSenderParam)
        {
            emailSender = emailSenderParam;
        }
        public void ResetPassword() { 
        //IEmailSender mySender = new MyEmailSender(); 
        //...Email Action... 
        emailSender.SendEmail(); 
        }
}

 

我们可以发现,没有了MyEmailSender,这就意味着我们打破了PasswordResetHelper和MyEmailSender之间的依赖性。PasswordResetHelper的构造器需要一个对象作参数,而这个对象是IEmailSender接口的的实现,它不用去知道这个对象是什么,或者说它根本不用去关心,并且也不用负责去创建它。

依赖在运行时就被注入到了PasswordResetHelper里面,也意味着那些实现了IEmailSender接口的类的实例将会被创建,并在PasswordResetHelper实例化期间传递给它的构造器。这样在PasswordResetHelper和任何实现了它需要的接口的类之间没有编译时的依赖。

用Asp.Net MVC实战一个简单依赖注入的例子

回到我之前做的竞拍,接下就是将依赖注入应用到我们的竞拍程序里面,我们的目标很简单,创建一个controller命名为AdminController,我们使用MembersRepository来持久化,为了解决AdminController和MembersRepository之间的耦合,我们定义一个接口IMembersRepository.具体的代码如下:

    public interface IMembersRepository
    {    public interface IMembersRepository
    {
        void AddMember(Member member);
        Member FetchByLoginName(string loginName);
        void SubmitChanges();
    }
    public class MembersRepository : IMembersRepository
    {
        public void AddMember(Member member)
        {
            //member.LoginName = "Huitai";
            //member.ReputationPoints = 100;
        }
        public Member FetchByLoginName(string loginName)
        {
            //自己写实现code
            //Member m = new Member();
            //.......
            //return m;
            return null;
        }
        public void SubmitChanges() { }
    }
        void AddMember(Member member);
        Member FetchByLoginName(string loginName);
        void SubmitChanges();
    }
    public class MembersRepository : IMembersRepository
    {
        public void AddMember(Member member)
        {
            //member.LoginName = "Huitai";
            //member.ReputationPoints = 100;
        }
        public Member FetchByLoginName(string loginName)
        {
            //自己写实现code
            //Member m = new Member();
            //.......
            //return m;
            return null;
        }
        public void SubmitChanges() { }
    }

是MVC那就写个Controller类,它依赖于IMembersRepository,如下代码所示:

public class AdminController : Controller
    {
        IMembersRepository membersRepository;
        public AdminController(IMembersRepository repositoryParam)
        {
            membersRepository = repositoryParam;
        }
        public ActionResult ChangeLoginName(string oldLoginParam, string newLoginParam)
        {
            Member member = membersRepository.FetchByLoginName(oldLoginParam);
            member.LoginName = newLoginParam;
            membersRepository.SubmitChanges();
            // ... now render some view 
            return this.View();
        }
    }

AdminController类的要求IMembersRepositoryinterface接口的实现作为一个构造函数的参数。这将在运行时进行,允许AdminControllerto操作一个类的一个实例,该类实现接口没有是耦合,实现。

关于MVC就先介绍这些吧!可能不完善,其他的知识后续继续补充,大家共同学习。要是那里有描述错误还是写错的地方,还请各位高手多多指点,批评,指导。也愿所有的新接触的同学学习什么的时候多带些问号?这样大家才能学到更好技术,共同进步!

posted @ 2012-06-26 12:36  辉太  阅读(2706)  评论(13编辑  收藏  举报

路漫漫其修远兮,吾将上下而求索!