ASP.NET MVC 3 Framework之旅 第四章

第四章

 

MVC模式

 

在第七章,我们将开始创建更加综合的ASP.NET MVC实例。在我们开始探寻ASP.NET MVC框架的细节之前,我们要确保你熟悉MVC设计模式和它背后的思维。在这一章,我们描述如下:

         MVC架构模式

         域模型和资料库

         使用依赖注入建立松耦合系统

         自动测试基础

 

         你可能已经熟悉了这一章中的一些概念和约定,尤其是如果你做过ASP.NET 或者C#的高级开发。如果没有,我们建议你仔细阅读这章。在继续阅读这本书时,很好的理解MVC的背景能帮助把框架的特征融合到环境中。

 

MVC的历史

术语模式-视图-控制器的使用最早可以追溯到1970年。它的出现来自于Xerox PARC的Smalltalk项目,在这里它的构想作为一种组织早期的GUI应用程序的方式。MVC模式中一些好的细节绑定在了Smalltake具体的概念中,如屏幕和工具,但是广泛的概念同样适用于应用程序,并且格外的适合网页应用程序。

 

         与MVC应用程序的交互遵循了用户操作的自然循环和视图更新,这里的视图假设为无状态。这非常适合的HTTP请求和响应来支撑Web应用程序。

 

         此外,MVC从UI界面强制关注域模型和逻辑控制器的分离。在网页开发应用程序中,这意味着HTML已经同应用分离,这使得维护和测试变得简单容易。正是Ruby on Rails推懂了MVC的主流地位,并且保存了MVC的原型。许多其他的MVC框架吸收和阐释的一个优点,当然也包括了ASP.NET MVC框架。

 

理解MVC模式

在高层次上,MVC模式意味着一个MVC应用程序将被分裂成至少段:

         模式,控制和表现用户处理数据。它可以是简单视图模型,只用于呈现在视图和控制器之间传递的数据;也可以是相对复杂的包含在业务领域里面,用来执行操作,传输,处理规则的数据。

         视图,作为UI界面,负责呈现数据。

         控制器,处理请求,执行对model的操作,并选择一个View来把model的数据呈现给浏览器或者说是用户。

 

         Models是对整个应用程序所工作的环境的定义,例如在一个银行系统中,model代表了程序支持的所有一切,如账户,总账和用户的信贷限额,以及可以用来操控数据的所有操作(诸如,存款和取款的操作)。该模型还负责维护状态和数据的一致性;例如,确保所有交易都添加到明细帐,和客户不用支付更多的钱当他又有这个权限。

         Models同样可以从它不负责的那方面来理解,比如它不负责呈现UI和处理各种请求,这些属于Views和Controllers的工作,Views除了负责展示model的成员给用户别无其他的工作,也就是说,Views是不知道Models的存在的,也不会跟Models有任何直接的联系。Controllers才是Views跟Models之间的桥梁,当有来自客户端的请求时,Controllers就会选择一个合适的View来响应用户的请求。

         MVC架构的每个部分都得到了很好的定义和划分,这也正是MVC一直贯彻的"分解关注点"的思想:对数据操控的逻辑都包含在Models里面;对数据的展示逻辑都包含在Views里面;处理用户请求和输入的逻辑都包含在Controllers里面。伴随着这样一个非常清晰明确的分工,我们的程序会变得更加容易维护和扩展,甚至是系统非常庞大了以后也一样。

 

理解领域模型

领域模式是MVC中重要的一部分。我们根据应用程序必须支持的行业或者活动中存在的现实世界的实体,操作,以及规则来创建模型,创建的Model也自然可以称为Domain Model。

      然后我们建立一个域的表现软件:领域模型。对于我们的目的,领模型是一个设置的C#类型(类,结构等等),统称为领类型。来自于域的操作通过在域类型中的模型定义来表现,并且域规则在模型内的逻辑中表现,或者如前面的章节中看到的,向模型请求C#属性。当我们建立了一个域模型的实例来表现一个特殊数据块时,我们建立域对象。域模型通常是持久的无生命周期的。有各种各样的方式来实现它,当关系数据库还是通常的选择。

      简言之,域模型是单一的,自动定义程序中的业务数据和进程。持久的域模型同样自动定义了域表现的规定。

      域模型方式解决了许多出现在页面平台的问题。业务逻辑包含在同一地方。如果你需要操作在模型中的数据或者添加新的进程和规则,域模型是程序中唯一个需要改变的。

 

注意:一个常用的方式从ASP.NET MVC应用程序中分离域模型是把模型放在C#组件中。这种方式,你可以创建域模型的引用来自于其他应用程序但是确保没有其他方面的引用。这对于大型项目是非常有用的。我们使用这种方式在第7章的例子中。

 

MVC的ASP.NET实现

在MVC中,controllers是一个C#类,来源于System.Web.Controller类。来自于Controller类中的每一个公共模型称为行为方法,这些都通过ASP.NET 路由系统同URL配置相联系。当发送的请求关联到一个行为方法,在控制器类中的语句执行,以执行一些操作上的域模型,然后选择一个视图来显示给客户端。图4-1展示了Controller,Model和View之间的联系。

图4-1 MVC应用程序中的联系

 

 

         ASP.NET MVC框架提供了支持可选的视图引擎。早期的MVC版本使用标准的ASP.NET视图引擎,它使用简约版Web窗体的标记语句处理ASPX页面。MVC 3引进Razor视图引擎,它使用了不同的标记语句(在第5章描述)。Visual Studio提供IntelliSense来支持两种视图引擎,使它成为一个简单的事件注入和响应来查看控制器提供的数据

         ASP.NET MVC不适用于域模型中运用的任何约束。你可以创建一个模型,用常规的C #对象和使用任何数据库,ORM框架,或者其他的支持数据的工具实现持久性通过.NET。VS建立一个Models文件夹作为MVC项目中模板的一部分。这是适合简单项目,但是许多的复杂的应用趋向于在VS项目中定义域模型。我们将讨论实施域模型在这章节后面部分。

 

MVC和其他模式的比较

MVC当然不是唯一的软件框架模式。还有很多其他的和一些最后成为主流的。通过研究其他的模式你能学到很多关于MVC的知识。在随后的章节,我们简短的描述不同的方式来构造应用和与MVC对比。有些模式是基于MVC的主题密切的变化,而另一些则完全不同的。

         我们并不认为MVC是完美的模式适用于所有情况。我们支持挑选最好的办法来解决手头的问题。正如你看到的,也有情况下,我们觉得一些竞争模式比MVC的有用或更好。我们建议你经过深思熟虑后选择模式。事实上,你在读这本书的时候你已经对MVC模式有了承诺,但我们还是认为广泛的观点是有用的。

 

了解智能的UI模式

一种常见的设计模式被称为智能UI。很多程序员已经在自己的职业生涯中建立智能UI程序,当然我们也有。如果你使用的是Windows Forms或者ASP.NET Web Forms,你也有。

         建立一个智能用户界面应用程序,开发人员构建一个用户界面,通常是通过拖动一套组件或控制到设计图面或面板。控件同用户之间的交互通过按钮事件,键盘输入,鼠标移动等事件。开发者添加代码来应答一系列的事件处理程序,这些小的代码块用于响应特殊组件中的特殊事件。就这样,我们结束了一个巨大的程序,如图4-2所示。处理UI和业务的代码混合在一起,没有任何关注点分离。代码接受输入控件中输入的数据,进行数据查询或修改用户账号,耦合在一起为了事件要求,结束于小块。

图4-2 智能UI模式

 

         这种设计最大的缺点就是很难维护和扩展。域模型和业务逻辑代码混合在UI代码中导致了重复,相同的业务逻辑片段被复制并粘贴到支持新增的组件。结果所有的重复部分和应用修补变得复杂。在复杂的智能UI应用程序中,在不打破现有的规则中它几乎无法添加任何新的功能。测试智能UI应用程序同样也变得困难。测试的唯一途径是SIMU后期的用户交互,这是很不理想和困难的基础,对于提供全面的测试覆盖率。

         在MVC的世界中,智能UI通常被称为反模式-----不惜任何代价避免这些。出现这种反感,至少有一部分,因为人们到MVC中寻找一种代替品,在花费职业生涯中的一部分来努力开发和维护智能UI后。对于我们来说这是正确的;我们长期受到无奈,但却没有伸手来拒绝智能UI。智能UI不是所有的都是糟糕的,还是有积极方面的。智能用户界面应用程序快速且易于开发。组件和设计工具开发商已经投入了很多的努力来使开发变得愉快,甚至连许多没有开发经验的程序员也能生成一些专业外观和合适的功能在短时间内。

         智能UI 应用程序最大缺点------可维护性没有一点提升。如果你生成的是一个简单的工具使用智能UI应用程序是一个完美的方案。MVC应用程序的额外的复杂性,根本没有必要。

         最后,智能用户界面是理想的用户界面原型------设计图面工具是非常好的。如果你是处于客户角度想获取界面的外观和流量的要求,智能用户界面工具可以是一个快速反应的方式来生成和测试不同的想法。

 

了解模式-视图架构

维修问题这方面往往出现在智能用户界面应用程序的业务逻辑,这最终使整个应用程序的扩散使得更改或添加新的特性变得艰辛。一个改善在这方面是提供模型视图架构,它把业务逻辑放在了一个独立的域模型中。这样做,数据,处理和规则都成为了程序中的独立部分,如图4-3所示。

图4-3 模型-视图模式

 

         模型-视图架构对于智能用户界面模式是一个大的改善。它容易维护,例如。然而,两个问题又产生了。第一是UI和域模型是紧密结合的,这样使得很难表示数据除非编译测试。第二是来自于实践,而不是模式定义。模型代表性地包含了大量的入口数据代码(可能不是必须的,但也是通常的),并且这意味着数据模式不包含业务数据,操作和规则。

 

了解经典三层架构

为了解决模型-视图架构的问题,三次或者三层模式分离出了域模型中的独立代码作为一个新组件称为数据访问层(DAL)。如图4-4所示。

图4-4 三层架构

 

         这向前迈进了一大步。三层架构被广泛的应用到商业应用程序中。他没有约束用户界面如何表现和提供好的关注点分离为了变得不复杂。并且,一些细节,DAL能被建立以至于使测试变得简单。你能看到经典三层架构应用程序和MVC架构有明显的相似。所不同的是,当UI层直接连接到一个点击和事件的GUI框架(如Windows窗体或ASP.NET Web窗体) ,它几乎是不可能执行自动化的单元测试。因为三层架构应用程序的UI部分可以是非常复杂的,拥有很多代码不能严格测试。

         在最坏的情况下,三层模式的UI层缺乏执行纪律,这意味着许多这样的应用程序结束于伪装智的能UI应用程序,没有真正分离关注。这给出来最坏的结果:不可测试,不可维护的,过于复杂的程序。

 

理解基于MVC的变化

我们已经探索MVC应用程序的核心设计原则,特别是因为它们适用于ASP.NET MVC实现。其他解释模式不同的方面,并已加入,调整,或以其他方式适应MVC ,以满足他们的项目的范围和主题。在接下来到部分,我们将提供关于MVC的两大主流改变的简单概述。了解这些变化是不是与ASP.NET MVC的工作至关重要。我们已经包含了完整的信息因为我们在别处听到了这些变化。

 

了解Model-View-Presenter模式

模型-视图-演示(MVP)是MVC中的一个变化,它的设计为了更容易适应于有状态的图形用户界面(GUI)表现如Windows窗体或者ASP.NET Web窗体。在没有带来问题的情况下,获取智能用户界面最好的方面这是值得尝试的。

         在这种模式下,Presenter同 MVC中的Controller具有相同的责任,但它直接关系到一个状态视图,根据用户的输入和行动直接管理在UI组件中显示的值。这种模式有两种实现:

                   被动视图实现,视图包含逻辑。用户界面控制器直接通过Presenter控制。

                   监控控制器的实现,视图需要负责逻辑上的一些元素,如数据绑定和获取来自于域模型的数据源。

 

         这两种不同的方式涉及到视图是如何智能的。无论哪种方式,Presenter在降低同图形用户界面框架的耦合,使得Presenter逻辑变得简单并且适合单元测试。

 

了解Model-View-View Model模型

Model-View-View Model模型(MVVM)是基于MVC的最新变化。它起源于2005年微软开发团队开发WPF和Silverlight技术时。

         在MVVM模型中,Models和Views在MVC中有同样的职责。不同的是MVVM中View Model的概念,它是用户界面的抽象表现。View model是一个典型的C#类显现将在用户界面表现的数据和操作来自用户界面请求的数据。不同于MVC中的Controller,MVVM中的View Model没有视图的概念(或者任何用户界面的技术)存在。MVVM中的View使用WPF/Silwerlight绑定功能双向联系通过View中控制展现的属性和通过view model展现的属性。

         MVVM同WPF绑定有着亲密的联系,所以它不是一个模式,很容易被应用于其他平台。

 

注意:MVC还采用view模型,但指的是一个简单的模型类,仅用于传递数据一个视图控制器。我们区分view model和domain model,它们都精确的表现了数据,控制和规则。

 

应用领域驱动开发

我们已经描述了域模型如何在应用程序中代表现实世界,包括对象,程序和规则的表现。域模型是MVC应用程序的核心。其他的一切,包括视图和控制器,也仅仅是一种同域模型交互的方式。

         ASP.NET MVC没有限制使用域模型的技术。我们可以自由寻找能同.NET Framework交互的任何技术,并且有多种选择。然而,ASP.NET MVC为我们提供基础设施和约定,以帮助连接在域模型中的控制器和视图的类和MVC框架本身。

它们有三个主要功能:

模型绑定是自动使用HTML表单提交的数据来组织领域对象(Domain Objects)基础的约定。

模型与数据让你描述模型类在框架中的意义。例如,你可以提供人类可读的关于属性的描述或提示应如何显示。MVC框架能自动的提供显示或者在视图中为你的模型类编辑UI。

验证,能够在模型绑定时被执行,并且能够应用被定义为元素据的那些规则。

         当我们在第3章建立第一个MVC应用程序时简单的接触了模型绑定和验证,我们将在17和18章回到这些话题和进一步了解。目前,我们将把ASP.NET MVC的实现放在一旁并且思考在领域的合适范围内建立成一个活动对象。我们将使用.NET 和SQL Server,使用来自于领域驱动开发(DDD)的一些核心技术来建立一个简单的域模型。

 

构建一个示例领域

你可能熟悉领域模型头脑风暴的过程。它通常包括开发,业务专家和大量的咖啡,饼干和白纸板。过一段时间,房间里的人们集中达到一个共识,并且一个关于域模型前期的草稿浮现。你可能以类似于图4-5  的形式结束,但它却是这个例子的开始:拍卖应用的一个简单域模型。

 

注意:在我们描述了草案的域模型中,我们跳过了许多小时似乎是不可避免的,在这个阶段在这个过程中的分歧和争论。我只想说,开发者将先花时间于来自业务专家的功能的需求上,而业务专家表示惊讶和关注来自于应用程序的时间和成本估计。解决这些问题咖啡是必不可少的。最终,每个人的膀胱是如此之饱,取得进展和达成的妥协,只是为了使会议结束。

图4-5 拍卖应用程序的初稿模型

         上面的模型包含了一个Members的集合,每一个Member会有一个Bids集合,每一个Bid对应一个Item,每一个Item可以有多个来自不同Members的Bids

 

通用语言

实现我们自己的domain model并作为一个独立的组件其中一个关键的地方是我们选择的语言和术语,这个不是我们的编程语言,而是领域建模的通用的语言。这种语言是开发人员和领域的专业人员都知道的,这样主要是为了这两种人能流畅的交流,而这却是至关重要的。当领域的专家们不了解建模的一些概念时,我们应该针对使用的术语达成一个共识,这个共识就是创建一种通用的语言并贯穿在整个领域建模过程中。这样做有很多的好处。

1.首先,开发人员倾向于使用编程语言,比如类名,数据库等等名词来表达。而业务专家们是不懂这些的,他们也不需要懂。业务专家知晓一些技术方面的知识是一件非常危险的事情,因为他们会经常根据自己对技术的理解来不断筛选他们的需求,这也就意味着需求会频繁的更改,进而导致开发人员也不知道业务专家的真实需求到底是什么。创建通用语言的方法能够帮助我们避免在一个应用程序里面过度的泛化需求,程序员倾向于建立每一个可能业务实际模型,而不是具体到某一个业务需求。

2.在通用语言和领域模型之间的这种连接不应该是非常肤浅的而是向DDD(Domain-Driven Design)专家所建议的那样:对通用语言的任何变化都会导致Model的变化。假如我们让建模跟业务领域不同步,我们就需要建立一种从Model到domain映射的中间语言,从长远来看,这种做法会导致灾难。为此,我们将创建一个会两种语言的特殊人类,他们随后就开始筛选需求,这却是建立在他们对两种语言都不完全理解的基础之上,当然这样的后果可以想象。

 

聚合与简化

图4-5为我们提供了一个很好的建模起点但是图中的模型并没有提供用C#和SQL Server实现Model的任何有用的帮助,会有不少的问题困扰我们:

1.如果我们load一个Member进入内存,也是不是应该load Member的Bids以及相关的Items进入内存呢?

2.如果我们这样做了,我们是否需要将这个Item其他的bids也load进内存,并且也将做这些Bids的Members一同load进内存呢?

3.当我们删除一个对象时,我是否应该删除相关的对象呢?如果是,又有哪些呢?

4.如果我们选择用文档存储代替关系型数据库来持久化,那哪一个对象的集合应该呈现到同一个文档呢?

所有以上的问题,我们不知道作何解答,并且我们的领域模型也没有给我们任何答案。

回答这些问题的DDD方式是将Domain Objects分配到组里面,这种方式称为聚合(aggregates)。图4-6很好的展示了怎样聚合在我们这个竞拍程序里面的领域模型,如下所示:

图4-6 拍卖领域模型聚合

 

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

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

聚合的一个好处是简化了对象跟领域模型之间的关系,通常这样能够帮助我们对需要建模的领域的本质的理解。本质上讲,创建聚合约束领域模型和对象之间的关系使得这种关系更加接近于现实领域里面存在的关系。清单4-1是用C#来表达的,如下所示:

清单4-1 拍卖与模型C#表达

public class Member {

    public string LoginName { get; set; } // The unique key

    public int ReputationPoints { get; set; }

}

 

public class Item {

    public int ItemID { get; private set; } // The unique key

    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之间的单向关系的本质,当然我们也可以建立一些其他的约束。例如:Bids是不可变的,这也是符合实际的。应用聚合能够帮助我们建立更加有用,更加精确的领域模型,也能够让我们用C#熟练的实现。

一般来讲,聚合为一个领域模型增加了结构化和精确化。这也使得应用验证变得容易(根实体负责验证聚合里面所有对象的的状态),这很明显也是持久化的单元。由于聚合的本质就是领域模型的原子单元,它们也能够适用事务管理的单元和级联从数据库删除的单元。

另一方面,聚合常常是人为加上限制。聚合(Aggregates)的概念能够很自然的从文档型数据库得到,但它不是sqlserver本身的概念,也不是存在大部分ORM工具里的概念,为了很好的实现它们,我们的团队需要科学有效的沟通。

定义仓库(定义存储类)

在某些时候,我们需要给领域模型添加持久化,这通常是通过关系型,对象型,或文档型的数据库来做。持久化不是我们领域模型的一部分,它是一个独立的关注点,这也就意味着我们不能将持久化的代码跟定义领域模型的代码混合到一起。解决这个问题通常的方式就是定义一个仓库(Repositories)

Respositories是基于数据库(可能你选择的是文件存储等等)层面的对象呈现。领域模型通过调用定义在Repositories的方法来间接的存储和查询数据库,这使得我们的Model可以独立于持久化的实现。这样约定就是为每一个聚合(Aggregates)定义单独的数据模型。在我们的竞拍程序里面,我们可以创建2个Repositories,它们分别是针对Members的Repository和针对Items的Repository。(注意这里,我们并不需要创建针对Bids的Repository,因为Bids会作为Items聚会持久化的一部分)。清单4-2展现了如何定义这些仓库。

 

清单4-2 C#定义的Member和Item域仓库类

public class MembersRepository {

    public void AddMember(Member member) { /* Implement me */ }

    public Member FetchByLoginName(string loginName) { /* Implement me */ }

    public void SubmitChanges() { /* Implement me */ }

}

 

public class ItemsRepository {

    public void AddItem(Item item) { /* Implement me */ }

    public Item FetchByID(int itemID) { /* Implement me */ }

    public IList<Item> ListItems(int pageSize,int pageIndex) { /* Implement me */ }

    public void SubmitChanges() { /* Implement me */ }

}

 

         注意Repositories仅仅针对Loading和Saving数据;它们不包含任何其他的逻辑。我们可以完成存储库类通过添加每一个方法声明来执行存储和检索操作的适当的持久性机制。在第7章,我们将开始建立一个更加复杂和现实的MVC应用程序,并且部分的过程我们将向您展现如何使用实体框架来实现你的存储类。

 

建立松散耦合组件

如我们所说,MVC平台中最为重要的一个功能就是能分离关注点。我们想程序中的组件能独立并且我们能操控一些相关性。

         在理想状态下,每一个组件不知道其他任何组件并且只能通过应用程序中的抽象接口来进行处理。这是被称为松散耦合,并且它测试和修改我们的应用程序更加容易。

         一个简单的示例将有助于事情的进展。如果我们写了一个组件被称为MyEmailSender发送的电子邮件,我们将实现一个接口,定义所有的公共功能需要发送e-mail ,我们称之为IEmailSender 。

         我们的应用程序需要发送电子邮件的任何其他组件(比方说,一个密码重置辅助称为PasswordResetHelper)然后能发送信息通过实现接口中的方法。而PasswordResetHelper 和 MyEmailSender没有任何的直接联系,如图4-7所示。

图4-7 使用接口分离组件

 

         通过引入IEmailSender,我们确保PasswordResetHelper 和 MyEmailSender不是直接依赖关系。我们可以更换MyEmailSender与其他电子邮件提供商,或甚至可以使用用于测试目的的模拟实现。

 

注意:不是所有的关系都要使用接口来实现分离的。这个决定取决于应用程序的复杂程度,需要什么样的测试和多长的维护期。例如,在简单ASP.NET MVC应用程序中我们将选择控制器同域模型不分离。

 

使用依赖注入

接口能够帮助我们解耦组件,但是这样仍然面临一个问题,那就是C#并没有提供一种嵌入的方式来比较容易的创建实现接口的对象,因为我们只能创建一个具体实现了接口的组件的实例,比如这里的的MyEmailSender的实例。最终的代码清单4-3.

 

 

 

代码清单4-3   获取接口实现的实例化具体类

public class PasswordResetHelper {

 

    public void ResetPassword() {

        IEmailSender mySender = new MyEmailSender();

        ...call interface methods to configure e-mail details...

        mySender.SendEmail();

    }

}

 

我们仅仅做了松耦合的一部分工作,PasswordResetHelper类通过IEmailSender来配置和发送邮件,通过接口的实现来创建对象,这里需要创建一个MyEmailSender的实例。

我们可能让事情更糟,因为现在的PasswordResetHelper同时依赖IEmailSender和MyEmailSender,如图4-8所示。

图4-8 紧耦合的组件

 

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

         DI是一个松耦合的设计模式我们通过在简单的示例中添加IEmailSender接口来开始。当我们描述DI,你可能会问,什么值得大惊小怪的,但原谅我们,因为这是一个重要的概念,是有效的MVC开发的核心。

         DI设计模式有两部分。第一部分从我们的组件里面移除任何的对具体类(PasswordResetHelper)的依赖性。在我们的这个例子里面,我们这样来做,将对必要接口的实现移动到类的构造器里面。如清单4-4.

 

清单4-4 移除PasswordResetHelper类中的依赖

public class PasswordResetHelper {

    private IEmailSender emailSender;

 

    public PasswordResetHelper(IEmailSender emailSenderParam) {

        emailSender = emailSenderParam;

    }

 

    public void ResetPassword() {

        ...call interface methods to configure e-mail details...

        emailSender.SendEmail();

    }

}

 

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

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

 

注意:PasswordResetHelper是通过它的构造器来实现依赖注入的,我们把这种称为Constructor Injection(构造器注入);我也可以通过它的公共属性来实现依赖注入,通常称这种方式为Setter Injection(设置注入)。

 

因为依赖是在运行时处理,这样我们就可以决定在程序运行的时候使用哪个接口实现。就像这里我们可以不同的Email Provider,或者是伪造一个仅仅用来测试。通过上面的方式我们目的就达到了。

 

一个特殊的MVC依赖注入示例

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

 

清单4-5 IMembersRepository接口

public interface IMembersRepository {

    void AddMember(Member member);

    Member FetchByLoginName(string loginName);

    void SubmitChanges();

}

 

public class MembersRepository : IMembersRepository {

    public void AddMember(Member member) { /* Implement me */ }

    public Member FetchByLoginName(string loginName) { /* Implement me */ }

    public void SubmitChanges() { /* Implement me */ }

}

 

         取决于IMembersRepository接口我们现在能写一个控制类,如清单4-6所示。

 

清单4-6 AdminController类

 

 

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

    }

}

 

         AdminController需要一个IMembersRepository接口的实现的对象作为构造器的参数。这个在运行时会被注入,也会允许AdminController对接口实现的对象的实例进行操作而不会发生耦合。

 

使用一个依赖注入容器

我们已经解决了依赖的问题,因为我们会在程序运行时注入构造器依赖,但是我仍然需要去解决另一个问题:我们怎样去实例化接口实现对象的实例而不在我们程序的其他地方创建依赖。

解决的办法就是DI容器,或者也称为IoC 容器。它实际上是一个组件,充当着依赖与实现依赖之间的"经纪人"的角色,我理解为就是中介代理什么的。具体到我们的例子里面就是,PasswordResetHelper需求(Demands)跟MyEmailSender之间的"经纪人"。

我们通过DI容器注册应用程序的使用的接口或抽象类的集合,这样就是要告诉DI容器选择哪一个合适具体类的实例来满足依赖。所以我们也应该通过容器注册IEmailSender,并且具体到当IEmailSender的实现被需要时,哪一个MyEmailSender的实例被创建,无论我们什么时候需要调用IEmailSender,比如创建一个PasswordResetHelper的实例,我们就去DI容器,它会给我们一个之前注册的作为默认的(接口实现类)的实例,我们的例子里面指的就是MyEmailSender。

         我们不用去实现DI容器,有很多好的开源的实现可以直接用。其中一个称为Nibject能直接在WWW.ninject.org中获取。我们将在第6章介绍如何使用ninject

 

注意:微软建立它自己的DI容器,称为Unity。我们使用Ninject是因为我们喜欢它并且它能混合和匹配工具在MVC中使用。如果你想了解更多的Unity。请看unity.codeplex.com

 

      我们可能会觉得DI的角色不重要,那你就错了,其实好的DI如Ninject具备一些非常"聪明"的特性:

      依赖链的解决

对象生命周期的管理

构造器参数的配置

 

      你可能有兴趣写自己的DI容器。我们认为这是一个伟大的实验项目如果你有一些时间来消磨并且想学到更多关于C#和.NET反射。如果你希望在一个MVC应用程序中使用DI容器,我们推荐你使用已经创建的DI容器,如Ninject。

 

本章还有一小节关于自动化测试的介绍没有翻译,关于这部分的内容还是百度或者谷歌一下,我自己看到有些模糊。

转载于:https://www.cnblogs.com/zhanghaomars/archive/2012/08/31/2665150.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值