一:MVC简史
术语“模型——视图——控制器(Model-View-Controller)”在20世纪70年代后期就已经出现。它产生于Xerox PARC(施乐公司的帕洛阿尔托研究中心)的SmallTalk项目,当时它被构想为早期GUI应用程序的一种组织方式。这个最原始的MVC模型有些少量细节依赖于SmallTalk特有的概念,如屏幕和工具,但是,最广泛的概念仍然适用于现在的应用程序——而且,特别适用于Web应用程序。
与MVC应用程序的交互遵循着用户动作和视图更替的自然周期,在这个周期中,假设视图时无状态的。这与支撑Web应用程序的HTTP请求与响应方式非常吻合。
二:理解MVC模式
从高级术语上说,MVC模式意味着一个MVC应用程序将分离成至少三个部分。
模型(Model):包含或表示用户使用的数据。这些可以是简单的视图模型(View Model),它们只表现视图与控制器之间传递的数据;也可以时域模型(Domain Model),它包含业务领域的数据,以及处理这些数据的操作、转换和规则。
视图(View):用于把模型的某些部分渲染成用户界面(UI)
控制器(Controller):处理传入的请求,执行模型上的操作,并选择渲染给用户的视图。
模型是应用程序工作世界的定义。例如,在一个银行业应用程序中,模型表示了应用程序所支持的银行中的任何东西,如用户账号、总账、信用额度,以及唔够用于操纵模型数据的操作,如账户的存款资金和应收款等。模型也负责保持数据的整体状态和一致性。
模型也由不是其职责的内容来定义:模型不涉及UI渲染或请求处理——那些是视图和控制器的职责。视图含有把模型元素显示给用户的逻辑,仅此而已。它们不直接感知模型,也不以任何方式与模型直接通信。控制器是视图与模型之间的桥梁——请求来自客户端,并且由控制器对其进行服务,控制器选择一个相应的视图向用户进行显示;而且,如果需要,会执行模型上的相应操作。
MVC架构的每一个部分都是定义良好和自包含的,这称为关注分离(Separation of Concerns),模型中操作数据的逻辑只包含在模型中,显示数据的逻辑只包含在视图中,而处理用户请求和用户输入的代码只包含在控制器中。利用各部分之间清晰的分离,不管应用程序变得多大,在其生命周期中都将更易于维护和扩充。
2.1 理解域模型
MVC应用程序最重要的部分是域模型。对于应用程序必须支持的业务或活动中存在的现实实体、操作以及规则等,通过对它们进行标识的方法来创建模型,这种模型被成为域(Domain)。
然后,要创建这个域的软件表示——域模型(Domain Model)。因此,域模型应当是一组C#类型(类,结构等),统称为域类型(Domain Type)。域中的操作由这个域类型中定义的方法来实现,而域规则表示成这些方法中的逻辑。当创建一个域类型的实例来表现一个特定的数据片段时,便创建了一个域对象(Domain Object)。域模型通常是持久化的,且一直处于活动状态——其实现由许多不同的方式,但通常选择关系型数据库。
简而言之,域模型是应用程序中业务数据及过程的唯一和权威的定义。一个持久化的域模型也是域所表现的状态的权威定义。
域模型方法解决了智能UI模式中出现的许多问题。业务逻辑只包含在一个地方。如果需要操作模型中的数据或添加新的过程或规则,域模型是应用程序必须修改的唯一地方。
2.2 MVC的ASP.NET实现
在MVC中,控制器是C#类,通常派生于System.Web.Mvc.Controller类。从Controller派生出来的类中的每一个public方法都称为一个动作方法(Action Method),它通过ASP.NET路由系统与一个可配置的URL相关联。当一个请求被发送到与一个动作方法相关联的URL时,便执行控制器类中的语句,以进行域模型上的一些操作,然后选择一个视图来显示给客户端。
ASP.NET MVC对域模型的实现没有任何约束。用户可以用常规的C#对象创建一个模型,并可以用.NET支持的任何数据库、对象关系映射(ORM)框架,或其他数据工具来实现持久化。
三:建立松耦合组件
正如前文所说过,MVC最重要的特性之一是它支持关注分离。人们希望应用程序中的组件应尽可能独立,而只有很少几个可管理的相互依赖性。
在理想情况下,每个组件都不了解其他组件的存在,而只是通过抽象接口来处理应用程序的其他区域,这称为“松耦合(Loose Coupling)”。松耦合使应用程序更易于测试和修改。
一个简单的例子可以帮助我们把事情放到相应的上下文中。如果正在编写一个“MyEmailSender”的组件来发送邮件消息,则会实现一个接口,它定义了发送邮件所需的所有public函数,这个接口称为“IEmailSender”。
应用程序需要发送电子邮件的任何其他组件,如名为“PasswordResetHelper”的密码重置程序,只需要通过引用这个接口的方法,就可以发送一份邮件。在PasswordResetHelper和MyEmailSender之间没有直接的依赖性,如下图。
通过引入IEmailSender,保证了PasswordResetHelper域MyEmailSender之间没有直接的依赖性,完全可以用另一个邮件发送程序来替换MyEmailSender,甚至为了进行测试而使用一个模仿实现。
3.1 使用依赖性注入
接口有助于解除组件耦合,但依然面临一个问题——C#没有提供内建的方法,以方便地创建实现接口的对象,除非创建具体组件的实例。于是要使用以下代码。
该例只处于组件松耦合的半途——PasswordResetHelper类要通过IEmailSender接口来配置并发送电子邮件,但是,为了创建实现这个接口的对象,就必须创建一个MyEmailSender的实例。
目前三者之间的依赖关系如下图。
此时需要一种获取实现给定接口对象的方法,而不必直接创建这个实现对象。这一问题的解决方案是依赖性注入(DI,Dependency Injection),也成为控制反转(IoC,Inversion of Control)。
DI是实现一种松散耦合的设计模式,当IEmailSender接口添加到这个实例时,便已开始了这个设计之路。
DI模式有两个部分。第一是从组件中消除对具体类的依赖性——此例是PasswordResetHelper。其实现方法是,把所有实现的接口传递给这个类的构造器,如下图所示。
这个实例打断了PasswordResetHelper与MyEmailSender之间的依赖性。PasswordResetHelper构造器需要一个对象来实现IEmailSender接口,但不知道也不关心这个对象是什么,而且不再负责创建它。
这种依赖性在运行时被注入到PasswordResetHelper中,即实现IEmailSender接口的某个类的实例将在实例化期间创建,并传递给PasswordResetHelper的构造器。在PasswordResetHelper与实现它所依赖的这个接口的任何类之间,不再有编译时的依赖性。
因为这种依赖性是在运行时处理的,所以可以在运行应用程序时决定使用哪个接口实现——可以在不同的电子邮件提供程序之间进行选择,或为了测试而注入一个模仿实现。
注:PasswordResetHelper需要用它的构造器来注入依赖性,这称为“构造器注入(Constructor Injection)”。也可以通过一个public属性注入这种依赖性,这称为“设置器注入(Setter Injection)”。
3.2 使用依赖性注入容器
前文已经解决了依赖性问题——在运行时把依赖性注入到类的构造器中。但还有一个问题需要解决——如何实例化接口的具体实现,而无需再应用程序的某个其他地方创建依赖性?
答案时“DI容器”,也称为“IoC容器”。这是在需要依赖性的类(如PasswordResetHelper)和这些依赖性的具体实现(如MyEmailSender)之间担任中间人的一个组件。
用这种DI容器来注册一组应用程序使用的接口或抽象类型,并告诉它,应该实现哪些具体类以满足依赖性。因此,用DI容器注册“IEmailSender”接口,并指明,在需要实现“IEmailSender”时,必须创建一个MyEmailSender的实例。无论什么时候需要一个IEmailSender,如创建一个PasswordResetHelper实例时,便进入DI容器,让它创建这个接口所注册的默认具体类的实例,即此例的MyEmailSender。
DI容器的作用似乎简单而平常,但事实并非如此。一个好的DI容器,如Ninject,有一些十分高级的特性。
依赖链解析(Dependency Chain Resolution):如果请求一个具体有自身依赖性的组件(如构造函数),这个容器也会满足这种依赖性。因此,如MyEmailSender类的 构造器需要一个INetworkTransport接口的实现,DI容器将实例化这个接口的默认实现,把它传递给MyEmailSender的构造器,并以IEmailSender默认实现作为返回结果。
对象生命周期管理:如果不止一次地请求一个组件,每次要得到的是同样的实例,还是一个新实例?一个好的DI容器将配置一个组件的生命周期,允许从预定义的选项中进行选择,这些选项包括singleton(每次同样的实例)、transient(每次新实例)、instance-per-thread(每线程实例)、instance-per-HTTP-request(每HTTP请求实例)、instance-from-pool(应用程序池实例)等。
构造器参数值的配置:例如,如果INetworkTransport接口实现的构造器需要一个叫serverName的字符串,用户应该能够在DI容器的配置中设置一个值。这是笨拙而简单的设置系统,它不需要你的代码传递诸如连接字符串、服务器地址之类的参数。
四:自动测试初步
ASP.NET MVC框架能够容易地建立自动化测试,并采用测试驱动开发(TDD,Test-Driven-Development)方法学。ASP.NET MVC为自动化测试提供了一个理想平台,而Visual Studio有一些很好的测试特性——它们使得进行设计和运行测试变得简单而容易。
从广义上讲,当今的Web应用程序开发者注重两种自动化测试。第一种是单元测试(Unit Testing),这是以与应用程序其他部分相隔离的方式,指定检验单个类(或其他小型代码单元)行为的方法。第二种是集成测试(Integration Testing),这是指定并检验多个组件,乃至包括整个Web应用系统,协同工作行为的方法。
这两种测试在Web应用程序中都有巨大的价值。单元测试便于创建和运行,当人们在算法、业务逻辑或其他后端基础结构上工作时,单元测试是十分精确的。
集成测试的价值在于它可以模拟用户与UI的交互,并可以覆盖应用程序所使用的整个技术堆栈,包括Web服务器和数据库。集成测试便于在旧的特性中侦测新的Bug,这称为“回归测试(Regression Testing)”。