在第七章,我们将创建一个更复杂的ASP.NET MVC示例,但在那之前,我们会深入ASP.NET MVC框架的细节;我们希望你能熟悉MVC设计模式,并且考虑为什么这样设计。在本章,我们将讨论下列内容
你或许已经对本章我们将讨论的概念和规范非常熟悉,尤其如果你有开发ASP.NET或者C#的经验。如果不熟悉,那么我们非常鼓励你仔细阅读本章,因为对MVC底层的理解有助于我们在后续的章节利用MVC框架的特性。
术语model-view-controller,自从20世界70年代就开始使用了,它诞生于Xerox PARC的Smalltalk项目,当时它被当作一种组织早期GUI程序的一种方法。MVC模式中一些优点都在视图应用在Smalltalk中,比如screens和tools,但是广义的MVC适用于所有的程序,更准确地说应该是适用于所有的Web程序。
与MVC互动应遵守用户行为和视图更新循环,在此处,视图被认为是无状态的。这与支持Web程序的HTTP请求和响应是相当匹配的。
再后来,MVC强制隔离概念—域模型和控制器逻辑应该从用户接口中分离出来。在Web程序中,这就意味着HTML和程序的其他部分分离开,这就使得维护和测试变得更加容易。Ruby on Rails引导了注意的兴趣到MVC上,并保留了MVC的一个分支。许多其他的MVC框架加入到MVC并从MVC受益,这些分支当然也包括ASP.NET MVC。
从上层来说,MVC模式就是MVC程序,该程序至少包含下面三个部分:
模型就是定义你程序工作有关各个事物。在一个银行程序中,模型就代表银行中程序需要支持的所有事物,比如账户,总帐,客户贷款显示,以及维护模型数据的操作,比如向账户存款和从账户中取款。模型还代表着保存数据的状态以及维护数据的一致性,比如确保所有的交易都记录到总帐,或客户取款数不能超过客户在银行存款的余额。
模型还可以通过它们并不负责的事情来定义:模型不处理呈现UI或者处理请求-这是视图和控制器的职责。视图包含用于向用户显示模型的元素—此外什么也不做。控制器用于连接视图和模型,控制器处理来自客户端的请求,并选择对应的视图以向用户显示,如果需要并调用模型的对应的方法。
MVC体系的每个部分都是清楚地定义和自我包含的-这就是概念隔离。操作模型中数据的逻辑仅仅包含在模型中;显示数据的逻辑仅仅包含在视图中,处理用户请求和输入的逻辑仅仅包含在控制器中。在每层之间都有一个明确的区分,你的程序就易于维护和扩展,无论程序规模的大小。
MVC程序中最重要的部分就是域模型。我们创建模型以识别现实世界中的实体、操作、以及存在于行业内部的规则、或者程序必须支持的行为,这些都是域。
我们接下来创建一个软件用于代表域(域模型)。为了满足此目的,域模型是一组C#类型(类,结构等等),所有这些被当作域类型。在域类型中定义的方法代表对于域的操作。当我们创建域类型示例时,该实例就代表某一特定的数据,创建实例就是创建域对象。域模型通常需要持久化或长期存在,这里有许多种方法可以满足这个要求,但是选择关系数据库是常见的方式之一。
简而言之,一个域模型就是在你程序内部对于业务数据和流程的一个单独的,权威的定义。持久化域模型就是关于域代表(域对象)状态的权威定义。
使用域模型这种方法解决了很多灵活UI模式的问题。因我们的业务逻辑值有一个,所以当你需要操作模型的数据或者添加一个新的哦流程或规则,域模型是程序中唯一需要更改的部分。
在MVC中,控制器就是C#类,通常都派生自System.Web.Mvc.Controller类。每个继承自Controller类的public方法都是行为方法,行为方法与一个可通过ASP.NET路由系统配置的URL联系在一起。当一个请求发送到与URL联系在一起的一个行为方法之后,那么在控制器中的语句将被执行,以对域模型进行一个操作,然后选择一个用于向客户端显示的视图。下图展示了控制器,模型和视图之间的互操作
在ASP.NET框架可以选择视图引擎。在早期的MVC版本中,使用标准的ASP.NET视图引擎,该视图引擎把Web Form标记语法按照流水线的方式处理ASPX页面。MVC3引入Razor视图引擎,在MVC4中对Razor进行了进一步的提炼,MVC4中的Razor语法(第五章会讨论Razor语法)已经和MVC3的语法完全不同了。Visual studio为这两个视图引擎都提供了只能感知,使得使用由控制器提供的视图数据变得非常容易。
ASP.NET MVC对于实现域模型没有做任何限制。你可以使用C#对象创建模型,使用数据库, ORM框架,或者其他.NET支持的数据工具完成对象的持久化。Visual Studio会自动为MVC项目创建Models文件夹。这对于简单的项目是非常适合的,但是对于复杂的程序应在一个单独的Visual Studio项目中定义域模型。我们会在本章后续内容中介绍如何实现域模型。
MVC并不是唯一的软件架构模式,当然,还有许多其他的模式,而且并且他们中的一些曾经相当流行。学习其他的模式有助于我们了解MVC。在本小节,我们将简要的介绍构建程序的不同方式,并将其与MVC进行对比。有一些模式是从MVC体系中变换出去,而剩下的则与MVC完全不同。
我们并不认为MVC是适合所有的情形。在解决问题时候我们需要寻找最好的方式。你即将看到在某些情形下,其他的模式与MVC一样游泳,甚至超越MVC。我们提倡在选择一个模式的时候应进行正式并精心地选择。虽然本书的目标是降讲述MVC模式,但是了解一下其他的模式也是非有益的
理解Smart UI模式
一种常见的设计模式就是灵活用户接口。许多程序员在其职业生涯的某个时间创建过灵活UI程序—通常通过拖动一组控件或组件到一个设计表面(窗口)这些控件通过点击按钮,移动鼠标,操作键盘所产生的事件与用户进行交互。程序员在事件处理器中编写代码以处理这些事件。在这么做的时候,如果我们构建大规模的程序,那么处理用户接口和业务的代码将会混合在一起,完全违背概念分离规范。定义可接受的数据输入,查询数据库修改用户账户,介绍一些小事情,根据时间发生的顺序捆绑在一起。
这种模式的最大缺点就是难以维护和扩展,由于它把域模型、业务逻辑代码混合在一起,而且在业务逻辑代码中处理用户接口的代码带来了许多重复(拷贝相同的业务逻辑代码片段,在新的组件中粘贴这些代码)。找到重复的部分并尝试移除重复是非常困难的。在一个复杂的UI程序中,几乎不可能添加新的特性而不破坏当前的特性。此外,测试Smart UI程序也非常困难,唯一的方法就是模拟用户的操作,然而这离目标相差甚远,而且难以实现一个完整的覆盖测试。
相对于MVC,Smart UI模式经常被认为是一种反模式—应该不计成本地避免使用该模式。 在花费了大量时间视图开发和维护Smart UI程序后,人们视图通过MVC寻找一个可替代方案,那么便会有抵触情绪。这对于我们也是一样的。我们都已经忍受很多年了,但我们不反对无法控制Smart UI模式。并不是Smart UI模式中的一切都是腐朽的,使用Smart UI还是有其好的方面。Smart UI开发容易而且快速——组件和设计工具已经完成了大量工作,这使得开发变得非常惬意,最重要的是,甚至许多没有经验的程序员也可以在很短的时间里开发出一些看起来很专业的程序
Smart UI程序的弱点——从维护性方面来讲——并不需要多少额外的开发工作。如果你为某些人创建一个小工具,那么smart UI程序就非常完美了。而简化一个MVC程序的复杂性则不一定能得到保证。
最后,Smart UI非常适合于用户接口原型——这些设计表明工具相当不错。如果你和客户坐在一起,并希望获取用户界面和界面控制流的需求,Smart UI工具可以快速地生成和测试用户的各种想法。
理解Model-View体系
Smart UI程序会遇到维护的问题。那么一个改进的模式就是模型视图体系。使用该模式,你可以把业务逻辑放到域模型中。这样做,数据、过程、规则完全集中到程序的一个部分。
模型视图体系相对于Smart UI模式是一个很大的改进——这是的程序易于维护。然后,它却带来两个新的问题。第一个就是UI和域模型紧密的联系在一起,这使得单元测试难以执行。第二个问题来自于实践,而不是这么模式的理论。模型一般包含大量的数据读取代码——这就表明数据模型并不知包含业务数据、操作和规则。
理解经典的三层架构
为了解决模型视图架构产生的问题,三层或三层模式从持久化域模型的代码中分离出来,并放到一个新的名为数据读取层的组件中
这有了一个很大的进步,三层架构是被广泛用户商业程序,它并没有限制UI如何实现,也没有提供如何分离三层。此外,DAL可以单独创建这使得但也测试变得相当简单。你可以很明显地看到MVC模式和三层架构之间的相似性。而两者的不同之处在于,当UI层与click-and-event GUI框架绑定在一起(比日Windows Form和ASP.NET WEB FORMS),这使得自动单元测试不可能实现。此外,由于三层架构中的UI可能会非常复杂,这将会使得许多代码不能被严格地测试。
最坏的情形就是,三层架构在UI层缺少强制的规范,这就意味着许多这样的程序变得和Smart UI一样,并未真正地实现概念分离。这就带来了最糟的结果——不可测试的,不可维护的程序。
理解MVC的变体
我们已经揭示了MVC程序的最好设计原则,特别是其如何被应用到ASP.NET MVC的实现中。对于该模式的其他的解释,要么完全不同,要么有所增加,或者有所调正,或者封装MVC以适应其他程序的范畴。接下来,我们会简单地介绍两个MVC的变体模式。理解这两种变体模式对于ASP.NET MVC并不是必须的,我们介绍它们只是因为信息的完整性以及你可能曾经听说过它们。
理解MVP模式
Model-View-Presentation(MVP)是MVC的一种变体,它适用于有状态的GUI平台,比如Windows Form或ASP.NET Web Forms,它利用了Smart UI模式的优点,而且没有Smart UI锁面临的问题。
在这种模式下,presenter与MVC的控制器有一样的职责,但是presenter与一个有状态的视图有更加密切的关联,根据用户的输入和行为直接管理显示在UI组件中的数据。该模式主要实现了下面两点
这两者的区别在于视图是否智能。任何一种方式下,presenter都从GUI框架中分割开来,这样就简化了presenter的逻辑,并方便对presenter进行单元测试。
理解MVVM模式
Model-view-view-model(MVVM)模式是最近从MVC演化出去的一个模式。它诞生于2005年,由微软开发WPF和Silverlight的team提出。
在MVVM模式下,模型与视图的角色和MVC中的模型与视图相同。差别在于MVVP创建了一个视图模型,它是用户接口的一个抽象,一般来说就是一个C#类,该类对外暴露两个属性,一个用于保存在UI中显示的数据;另外一个可以被UI调用的对于数据的操作。与MVC的控制器不同,MVVM视图模型没有这样的概念——一个视图必须存在。MVVM视图使用WPF/Silverlight的绑定特性以双向地关联视图中控件的属性(下拉列表中的项目,或者按下按钮的效果)和视图模型中的属性。
MVVM紧密地与WPF绑定关联在一起,因此并不适用于其他的平台。
我们已经介绍了领域模型是如何在你的程序中呈现真实的世界:包括呈现对象,呈现过程和呈现规则。域模型是MVC程序的核心,所有的食物,包括视图和控制器,都需要和域模型交互。
ASP.NET MVC并没有限定使用域模型的技术。你可以选择任何可可以与.NET Framework交互的技术,这当然就有很多种选择。但是,ASP.NET MVC提供了规范和框架以帮助把域模型中的类与视图和控制器联系起来,以及连接类和MVC框架本身。这主要包含以下三点:
我们在第二章,创建第一个MVC程序中,简单地尝试了模型绑定和验证。在第22章和第23章我们会更深入的介绍这它们。在本章,我们把注意力集中在ASP.NET如何实现MVC模式上,并思考如何创建域模型。接下来,我们将使用.NET和SQL服务器来创建一个简单的域模型。
你可能经历过创建域模型的头脑风暴。它经常会有程序员,业务专家,咖啡,白板和水笔。很快,与会的人员就会达成一个共识,然后首先给出一份关于域模型的草稿。最终,你可能会得到如下图的结果,这也是我们创建简单域模型的起点。
该模型包含一组Member,每个都有一组Bids,每个Bids都关联一个Item,每个Item可以有来自不同Members的多个Bids。
把域模型当作一个单独的组件实现的主要好处是, 你可以根据你的具体情况来选择语言和专门的术语。你应该找到对象,操作和关系的术语,以使得域模型不仅对程序员而且对业务专业都有意义。我们推荐你应当选择已经存在于"域"中的术语,比如,如果一个程序员在域中,已经把用户和角色描述为代理人和许可。那么在域模型中,你也就应当使用代理人和许可。(而不是用户的角色)
当在域内创建一个概念而业务专家又没有为你推荐一个专门的术语时,你应当遵循常规:创建通用语言,并始终贯穿整个域模型。
程序员喜欢使用代码来描述域模型:类的名字,数据库表等等。而业务专家又不理解这些术语(他们也不需要理解)。业务专家理解一些技术其实非常危险,因为这可能导致业务专家根据他所理解的技术直接过滤掉一些需求。当这种情形发生时,你就不能真正地理解业务需求。
通用语言还有助于在一个程序中过度概化,程序员一般都倾向于把全部业务都建立模型,而不是只建立业务需要的模型。在真正建模的时候,我们需要把Members和Items更换为资源和关系。当我们创建域模型,而该模型又不仅限于在该语模型中使用时,那么我们就失去了获取洞悉真正的业务流程——那么,在将来,我们就会随着业务的变化而更改模型从而是模型变成真实世界的抽象。限制不是限定,这可以使你在开发中朝着正确的方向不断的前进。
通用语言和域模型之间的联系不应该是表面的,DDD专家建议更人关于通用语言的更改都会导致在模型中发生相应的更改。如果通用语言发生更改,而模型没有变化,那么你就创建了一个中间语言,语言把模型映射到域,但这对于长期来说确实一个灾难。就相当于你创建了一个人的特殊类,该人可以将两种语言,但是因为他们不能充分地掌握两门语言,从而导致他们会开始过滤需求。
前面的类图提供了模型行的良好的开端,但它并未对使用C#和SQL Server实现需求有任何指导意义。如果加载Member对象到内存,那么我们是否需要把Member的关联对象Bids和Items对象也加载到内存中?如果加载,我们是否需要为Member加载所有的Bids,或者仅仅是加载用户投标的Bids?亦或当我们删除一个对象,那么我们师傅哦需要同时删除关联对象? 如果我们选择文件系统而不是数据库系统来实现持久化,那么集合中的那些对象需要保存在同一个文件中?通过域模型,我们都不能获取这些问题的答案。
采用DDD方式使,DDD把对象分组(聚集)后,就可以回答这些问题。下图就展示了分组后的域模型
一个聚聚后的实体把多个域模型对象分组——在模型中,存在一个用于识别整个聚聚关系的根实体。它就相当于一个老板,其作用为验证和保存操作。对于数据更改,聚聚被当作一个单独的单元,因此我们需要创建代表关系聚聚,这就使得模型莫的上下文变得有意义,还需要创建操作以响应业务流程的逻辑。这也就是说,我们需要把像一个组那样发生变化的对象通过分组从而创建聚聚。
DDD的一个重要规则是魔一个特定聚集的实例之外的所有对象都仅仅能对引用根实体,而不是聚聚内的其他对象。该规则强制强化了在聚聚内部一个聚聚应当被当作一个单独单元的概念。
在我们的例子中,Members和Items都是聚集根对象,而Bids仅仅可以在Item上下文中被访问(Item就是Bids的根对象)。Bid允许引用Members,然而Members不能直接引用Bids。
聚集的一个优点就是简化了域模型类对象之间的关系。而且,通过聚集还可以更深入的了解域的特性。总地来说,创建聚集约束了域模型对象的关心,从而使得域对象之间的关系与现实世界中的关系一样。下面的代码展示了域模型应该是什么样的
publicclassMember
{
publicstring LoginName { get; set; }
publicint ReputationPoints { get; set; }
}
publicclassItem
{
publicint ItemID { get; set; }
publicstring Title { get; set; }
publicstring Description { get; set; }
publicDateTime AuctionEndDate { get; set; }
publicIList<Bid> Bids { get; set; }
}
publicclassBid
{
publicDateTime DatePlaced { get; set; }
publicDecimal BidAmount { get; set; }
publicMember Member { get; set; }
}
需要注意的是,我们如何轻易地捕获Bids和Members之间的(单向)关系。我们还可以构建其他的约束,比如,Bids是不可更改的(体现常见的拍卖转化:一旦报价,次报价就不可更改)。应用聚集,可以使我们创建爱你更有用的和更准确的域模型,这样在具体的语言中使用简短的代码就可以呈现这些域模型。
一般地,使用聚集为域模型增加了结构和准确性。聚集使得应用验证变得更为容易,很明显聚集是以单元地形式持久化。此外,因为聚集对于单元的原子性是必须的,此外,聚集适合于数据库的事务以及级联caozuo.html" target="_blank">删除操作。
在另外一个方面,聚集所揭露的限制是人为的——其实大多数都是人为添加的。但聚集对于SQL Server或ORM工具,不是原生的概念。因为为了实现聚集,团队里应该加强训练和沟通。
在有些死后,我们需要添加持久化域模型的代码,这个一般通过关系数据库,对象数据库或文件数据库完成。持久化不是域模型的一部分,而是概念分离模式中独立的概念。它意味着我们不希望把处理持久化的代码和定义与魔模型的代码混合在一起。
常见的强制分离域模型和持久化系统的方式就是定义Repositories——代表背后数据库(或者文件存储,或其他用于持久化的东东)的对象。域模型不直接与数据库打交道,而是通过Repositories的方法工作,这些方法调用数据库存储和获取模型数据——这就使得模型和持久化的具体实现互相隔离。
该规范还定义了,对每个聚集,数据模型应该独立。因为聚集就是持久化的单元。在我们拍卖的例子中,比如,我们可能会创建两个Repositories,一个用于Members,另一个用于Items:
publicclassMemberRepository
{
publicvoid AddMember(Member member)
{}
publicMember FetchByLoginName(string loginName)
{
returnnull;
}
publicvoid SubmitChanges()
{}
}
publicclassItemRepository
{
publicvoid AddItem(Item item)
{}
publicItem FetchByID(int itemID)
{
returnnull;
}
publicIList<Item> ListItems(int pageSize, int pageIndex)
{
returnnull;
}
publicvoid SubmitChanges()
{}
}
请注意,Repositories只关注加载和保存数据——完全没有任何域逻辑。我们可以通过添加执行存储或获取数据的操作以完成Respository类。在第七章,我们将创建一个复杂的MVC程序,而且在那里,我们会展示如何视同EF实现repostiories。
在前面我们曾将介绍过,MVC模式的一个最重要的特点就是它允许概念分离。我们希望系统中的组件尽可能地相互独立,这些组件有了较少的依赖之后就便于我们管理。
在一个完美的解决方案中,每个组件都不需要了解其他的之间,只需要通过抽象接口与其他的组件相互操作。这就是松耦合,它使得程序的测试和维护都遍地非常容易。
下面我们来看一个简单的例子。如果我们城建一个名为MyEmailSender的组件,该组件用于发送电子邮件,那么我们定义一个接口IEmailSender,然后实现该接口。那么程序中其他的组件需要方式邮件的时候,通过调用接口中发送邮件的方法直接发送邮件。在这个例子中,发送邮件的组件和实现接口的组件之前就没有任何依赖关系
使用接口可以使我们解耦组件,但是我们需要面对一个问题——C#并没有提供内置的方法用以创建实现接口的对象,只能通过创建一个组件实例。
publicclassPasswordResetHelper
{
publicvoid ResetPassword()
{
IEmailSender mySender = newMyEmailSender();
mySender.SendEmail();
}
}
PasswordResetHelper使用IEmailSender接口发送邮件,但是为了创建实现接口的对象,我们必须创建MyEmailSender的实例。这么做其实挺糟糕,因为PasswordResetHelper依赖IEmailSneder和MyEmailSender
我们真正需要的是不用直接创建就可以获取实现接口的对象。解决这个问题的方法就是DI(也叫做依赖注入)。也被称作IoC(控制器反转)
DI是一个设计模式,它通过向我们之前描述的简单例子中添加IEmailSender接口来完成松耦合的实现。当我们描述DI时,你可以能为猜想这是什么东西啊,乱七八糟的。但是DI对于高效地开发MVC是一个非常重要的概念。
DI包含两个部分,一方面是我们在组件中移除和具体类相关的依赖(在我们的列子中,就是要移除对MyEmailSender的依赖)。为了实现这一点,我们可以在构造函数中接收一个接口的实现类。
publicclassPasswordResetHelper
{
privateIEmailSender emailSender;
public PasswordResetHelper(IEmailSender emailSenderParam)
{
emailSender = emailSenderParam;
}
publicvoid ResetPassword()
{
emailSender.SendMail();
}
}
这样我们就移除了PasswordHelper和MyEmailSender之间的依赖关系,ResetPasswordHelper的构造函数要求一个实现了IEmailSender接口的对象。那么现在,ResetPasswordHelper不用知道,也不用关心,这个对象是什么,也不用负责创建具体的对象。
依赖在运行时才注入到PasswordResetHelper中,这样就是说,实现IEmailSender的实例将被创建,在实例化ResetPasswordHelpershi,并传递给ResetPasswordHelper的构造函数。那么对于PasswordResetHelper和实现IEmailSender的接口的类之间,不存在编译时的依赖。
因为依赖实在运行时处理,那么我们就可以决定在程序运行时到底是用哪个实现接口的类——也就是说我们可以选择不同的实现方式,这也方便了测试。
现在让我们回到我们之前创建的拍卖域模型中,现在我们把DI应用到这个模型中。现在的目标是创建一个控制器类,我们将其命名为AdminController,该控制器类使用MembersRepository持久化,并且使两者之间不存在依赖关系。首先我们定义一个即可偶用于解耦这两个类——我们把这个接口命名为IMemberRepository——并更改MembersRepository类实现该接口
publicinterfaceIMembersRepository
{
void AddMember(Member member);
Member FetchByLoginName(string loginName);
void SubmitChanges();
}
publicclassMembersRepository : IMembersRepository
{
publicvoid AddMember(Member member)
{
thrownewNotImplementedException();
}
publicMember FetchByLoginName(string loginName)
{
thrownewNotImplementedException();
}
publicvoid SubmitChanges()
{
thrownewNotImplementedException();
}
}
然后,我们开就开始创建依赖于IMembersRepository接口的控制器类
publicclassAdminController : Controller
{
privateIMembersRepository membersRepository;
public AdminController(IMembersRepository repository)
{
membersRepository = repository;
}
publicActionResult ChangeLoginName(string oldLogin, string newLogin)
{
Member member = membersRepository.FetchByLoginName(oldLogin);
member.Loginname = newLogin;
membersRepository.SubmitChanges();
return View();
}
}
AdminController类需要一个实现了IMembersRepository接口的类的实例。这个实例会在运行时注入,那么,现在AdminController可以调用实现了接口的实例,但是却没有依赖于接口的具体实现。
我们已经解决了依赖问题——通过在运行时把依赖通过构造器注入。但是我们还有一个问题没有解决,我们如何通过不在程序中创建依赖,从而实例化实现接口的具体类?
答案就是依赖注入容器,也就是众所周知的IoC容器。它就相当于历来关系中的一个经纪人,比如一个类PasswordResetHelper, 和依赖关系的具体实现,比如MyEmailSender。
我们注册一组接口或者抽象类型以供程序和DI容易使用,并且告诉DI容器哪一个具体类将被实例化以满足依赖关系。因为,我们需要者在容器中注册IEmailSneder接口,并指定无论何时当需要一个IEmailSender的具体实现时,都会创建一个MyEmailSender实例。当我们需要IEmailSender时,比如创建PasswordResetHelper实例时,我们就进入DI容器,然后我们就会得到获取在容易中注册类的实现作为该接口的具体实现,在我们的例子中,具体IEmailSender的具体实现就是MyEmailSender。
我们并不需要自己创建DI容器,因为已经有许多有名的免费开源实现。比如Ninject,你可以从jinject.org获取到更多详细信息。在第六章我们也会详细介绍它。
你可能认为DI容器的职责及简单有微小,但实际上并不是这样的,一个好的DI容易,比如Ninject,应拥有下面的特性
你可能会尝试创建自己的DI容器。我们认为那将会是一个非常有意义的项目,如果你有时间完成,并希望对C#和.NET反射有更深入的理解。如果你想在MVC生产环境中使用DI容器,那么我们还是建议你使用一个稳定的DI容器,比如Ninject。
略