什么是代码结构的组织?
asp.net MVC 5 默认创建出的几个目录的标准含义分别如下:
- Controllers目录存放MVC模式中的Controler
- Models目录存放MVC模式中的Model
- Views目录存放MVC模式中的View
除此外还有Content(存放资源文件,如CSS、图片)、Scripts(JS脚本文件)等目录是有标准含义的,有标准含义也就是说有非标准含义,你可以遵循这些标准含义也可以定义自己的非标准含义,比如:
- asp.net MVC 5 中的Controller无非是继承system.web.mvc.controller的类而已,你可以把这个class放在其他目录甚至另外一个Project中。
- asp.net MVC 5 的Model,常见做法是将它独立在一个Class Lib类型的Project中
- asp.net MVC 5 的View默认为Razor引擎,在默认的规则中用Views目录存放所有的视图,你可以通过修改规则来将视图放在其他目录中,或者你用其他的视图引擎甚至自定义视图引擎
这就是我所谓的代码结构的组织,本质上其实就是MVC模式(在看MVC各种理论的时候注意一个心态:MVC模式本身也是发展变化的,每个具体的MVC模式的实现(比如asp.net mvc)或多或少都在理论的基础上做了修改,和理论不能完全吻合上很正常,我在学习之初太过死板,在这方面纠结了很久时间)在asp.net MVC中的实现,当然远没有上面所述的仅仅是几个目录的分类这么简单,比如Model,如果仅仅是把它独立在一个Class Lib中的话,仔细想想,我们以前不都是这么做的吗?
有个PHP平台的框架YII所遵循的一些MVC实践原则我觉得是总结的比较实用的(以下内容是这些实践内容的核心,部分为我自己对原文的理解加变化):
模型(Model)用于表示底层数据结构,经常在整个应用的不同部分共享,有些模型在前后台、API中都会用到,所以一个Model应该遵循的指导原则有:
- 包含属性用于描述特定的数据
- 应该包含业务逻辑,以确保数据能够满足表现的需要
- 应该包含数据操作的代码,比如数据存储、检索
- 确保在不同的环境中均可实用,比如B/S环境、控制台应用或者作为单纯的API
- 不应该出现HTML代码,负责表现的代码应该放到view文件中(这点是WebForm时代为了实现动态响应而经常这么处理的,而现在我们有Javascript框架可以选择,如JQuery)
在上述指导原则下,可能会写出非常庞大的Model类(过多数据操作,业务逻辑代码包含其中)。这种情况下,建议进一步抽象,提炼出一个基类,包含最通用的功能,然后前端、后端和API在用到时候,将各个子应用才相关的逻辑放到基类继承出来的子类里面。
视图(View)主要就用于前端表现的代码:
- 包含HTML,以及所有负责表现的代码,可以出现.net语法,但是只用于遍历数据、格式化数据(在asp.net mvc + JQuery环境中,用JQuery语法来遍历、格式化、构造Html呈现数据而不用.net语法)
- 不应该包含DB请求
- 仅关于表现,布局等和页面呈现有关的业务出现在View中,用户的请求数据应该由Controller和Model负责处理
- 如果必要,可以访问Model和Controller的属性,不过这是为了满足表现的需要(asp.net mvc 5 默认创建的view就是直接访问Model的属性来呈现数据的,老实说这让我很Confuse<困惑、糊涂>:MVC模式本身不是为了解耦的吗?且很多说法都是视图都是通过Controller来与Model关联而不是直接与Model关联。在asp.net mvc + JQuery环境中,所有视图通过JQuery访问Controller来获得返回数据以呈现,也通过JQuery访问Controller来修改数据)
可以使用诸如布局、部分视图等框架特性来最大程度重用View的代码。
控制器(Controller)是将模型、视图和其他组件组装在一起形成一个应用的粘合剂。控制器直接负责处理终端用户的请求。
- 处理终端用户发出的GET或POST请求,GET用来获取数据,POST用于修改数据
- 创建模型,并决定一个模型对象的生命周期
- 不应该出现SQL语句,数据库请求应该放到Model中(在WebForm的实践做法中,我们创建了一堆的API、方法、函数,很多也没有出现SQL语句,在考虑重构时这很让我抓狂:什么样的代码应该放在Controller中,什么样的代码应该放在Model中,即便有了这个原则之后也是)
- 不应该出现HTML代码,而应该将其放入到View中
在一个设计良好的MVC应用中,控制器是非常轻量级的,经常只有几十行代码的样子;而Model总是非常复杂而且庞大,包含了所有的用于表现的数据及其操作方法。这是因为由数据结构和业务逻辑组成的模型对每个应用来说,都是独特的,需要大量的定制化工作来满足应用的需求;控制器的逻辑经常遵循一个特定的套路,在各个应用中都差不多,因此可以被框架底层代码极大程度地简化(也就是说不是控制器代码少,而是Web开发框架已经都抽象出来并且都帮你做好了,这也 就是框架的价值和能够实现快速开发的原因)。
遵循以上代码结构组织实践,可以有一个基于asp.net mvc 5构建的Web Application,这个project主要处理Controller和View,关于Controller的实践原则以上四条是远远不够的,Controller的作用是通过调度来达到控制输出(视图或数据)的目的(当我们说这句话时,实际上指的是Controller的Action的实践原则。另注意:这里提及的调度和控制两个词不是技术术语,就是通俗语言的说法而已),举例来说:
- MVCStore示例将身份验证作为一个AuthenticationController,此控制器内不同的方法(Action)可实现不同的身份登录(想象一下:AD登录、Form登录,这是企业内部应用常见的两种身份验证方式,我最近参与的一个对公网的系统还涉及到与微信登录的集成),代振军有篇博文对MVCStore的这种做法做了不错的分析,由此我总结的一个Controller开发原则(出发目的是用来应对让我抓狂的"到底哪些代码放入Controller,哪些代码放入Model"的困惑):业务流程放入Controller中,而业务流程中的具体业务规则放入Model中(这是符合SOA构的思想的---好大的帽子吧,可以阅读上面提到的代振军的那篇博文)。基于这个原则,不管是哪一个登录方法,其调度控制的业务流程都应该是这样:
- 接收客户端发出的身份信息(通过调度实现):Oxite示例的做法是通过调度,而MVCStore是直接在Action中Request.Form获取,缺点在于会让Action中的代码增长过快
- 判断此身份是否合法(通过调度实现):MVCStore中体现的很明显,在Model中创建有对应AuthenticationController的AuthenticationService,通过对此Service的调用来完成此业务流程步骤,而具体的业务规则就放在了Model中(根据此原则可将Model划分为两部分:一是数据模型实体部分,二是业务逻辑部分,如下图所示)
- 如果身份合法则认可(这是控制):"如果合法"是业务流程,而"认可"的具体内容(主要指的是对登录身份的保持)则是业务规则,认可后当然就是控制输出视图了(这部分代码我也把它并入到业务流程中)
- 如果不合法则不认可(这也是控制):"如果不合法"是业务流程,而"不认可"一般不包含什么业务规则,直接开始控制视图输出
- Codeplex上还有一个叫ASP.Net MVC 基础模板 (MVC4,EF 5.0)的项目,其Controller和View放在一个Project中,使用了基于Area的路由映射,根据功能模块来将Controller和View进行组织,是个有效的降低大型项目结构复杂的方法
对于Controller在实际项目中需要小心时刻留心要始终保持Controller与View的轻快,小心随着业务复杂度提高而产生的困惑:有很多代码你不知道把它放哪,就只好把它放到控制层,最后发现在控制层中塞了太多的代码。有个叫Irwin的人提出了MOVE模式(这里有个简单的翻译版本)作为MVC模式的改进,其出发点也基于此,在这个MOVE模式中MOV从概念定义可以判断出就是对应了MVC(Operations对应着Controller),增加的一个Events似乎是用来使视图和模型直接关联,我猜想作者可能是在开发Controller时很多的Action需要根据复杂业务逻辑对视图产生影响,因而写了很多此类的代码在Controller中,小心的对Controller和View进行结构化组织,就可在很大程度上避免这种情况。
接下来针对我手上的系统的重构做些实际的思考来应用以上这些原则。