现在的位置: 首页 > 综合 > 正文

WEB MVC

2012年10月10日 ⁄ 综合 ⁄ 共 8565字 ⁄ 字号 评论关闭

从事Web开发,一般都会接触到MVC框架这个概念。

M:也就是Model,直接跟网站数据库相关。

V:也就是View,是网页的模版,跟显示数据相关。

C:则是Controller,相当于网站的业务逻辑。

MVC也不仅仅是应用于网站开发,它的概念实际上植根于桌面软件,并且在手机软件开发上也有应用。

MVC本身是一个设计模式,是一个被验证过的,可以用来很好归纳、管理代码的软件开发方式。

基于这样的设计模式,提供了很多相关的类库实现,则“设计模式”升级为“框架”。

MVC的任何一个方面,扩展出去讲,都可以讲上几天几夜。

今天只讲V。

传统的ASP / PHP网站开发,V是很混乱的。

默认只有一种文件,html与业务逻辑代码混杂在同一个文件;相当难以维护。

ASP.NET相对于asp做出了很大改进,提出了code-behine的概念:默认将html的模版代码,以及c#或者vb.net的逻辑代码切分到两个不同的文件。

这样的方式算是有很大进步。

微软平台上做开发是比较苦逼的,微软掌控了整个开发平台的前进速度。

asp跟PHP在开始的时候,是相似的技术。有类似的便利,以及类似的麻烦。

微软推出了.net,推广了code-behind的模式;然后,所有的微软程序员都超着微软指定的这个方向去迈进。

asp被抛弃了,自从ASP.NET诞生之后,就不再有任何改进。

而PHP,在开源世界中,则不断的得到各式各样的改进。

各种模版引擎层出不穷;不仅可以实现code-behind这样简单的模版、业务代码分割;很多还直接引入了MVC的概念;实现了三层的分割。

而ASP.NET,则长期止步于web form的code-behind,在开源世界中的MVC方案大放光彩若干年后,才推出 ASP.NET MVC。

模版技术,最初的目的就是要把业务代码,也就是说,把获得数据的代码跟html分割。

在模版实现上,因此涌现了不少不同的设计哲学。

Python的Django框架中的模版,是一种典型。

它彻底的禁止程序员在模版中嵌入任何代码;模版中,只可以出现html;以及一些跟业务逻辑无关的控制标签,如:

  1. {% If XXXX %} foo {% else %} bar {% end %} 

条件XXXX,必须是一个数据值,不可以是一个复杂表达式、不可以包涵函数调用等等。

模版中,也不可以声明任何新的变量,下面的做法是被禁止的:

  1. {% set i = 0 %}  
  2. {% foreach item in items %}  
  3.  {% i += 1%}  
  4.  <div>  
  5.   {{ item }}  
  6.   {% if i mod 2 == 0 %}  
  7.    <hr />  
  8.   {% end %}  
  9.  </div>  
  10. {% next %} 

Django的模版,从技术上彻底禁止程序员添加任何逻辑,强迫程序员必须在controller中去写各种逻辑,以确保模版内容的纯洁干净。

所以Django的模版,一般都非常简单,有很好的移植性,并且可以让网页设计人员直接编辑。

ASP.NET则是另一种典型;虽然有了code-behind,但是它没有对前端代码,以及后端代码做任何限制。

在前端aspx页面中,可以嵌入任意的逻辑代码,而code-behind的code,为空白;这种伪“code-behind”的方式,跟原来的asp没啥区别。

ASP.NET从框架本身,并不阻止程序员去做这样的事情,实际上,它还标榜它这样的特性:方便原有的asp项目直接升级到.NET的平台上。

也有另外一种奇葩的做法,前端aspx页面保持空白,然后在code-behind的code中去拼接所有的html。这样的方式,ASP.NET框架本身也不禁止。

只要ASP.NET程序员喜欢,没有什么不可以的。

ASP.NET把对模版使用方式的选择权留给了程序员,如果程序员自律,他们可以按Django模版那样的方式去使用模版,并拥有Django一样的优点;如果程序员自律?!

在某些可以通过嵌入代码去快速处理的场景,ASP.NET的模版也保留了程序员去hack的能力。

还有一些模版技术,则是折衷的(如tornado的模版):允许嵌入单行代码,如声明变量,调用函数等等;但是不允许整块、整块的业务代码出现模版中。

上述三种模版设计哲学,各有它们的道理,以及应用场景。

需要根据具体的业务、应用场景,才能说其中哪种比较合适。

开发人员的能力也是直接相关的,如果团队中,普遍不自律;缺乏将业务、模版代码分割、以提高代码可维护性的意识,那么Django的做法是最好的,它直接禁止去滥用模版,强迫他们去使用更好的开发风格;即便在某些场景下会更麻烦。

武断的认为任何一种模版设计哲学是“最佳”的想法是极其肤浅的。

各种成熟的模版技术,一般也都会有包括以下特性:

1. 嵌入

也就是说,编写各种可以复用的小模版块,然后供多个不同地方调用;比方说,用户头像(甚至名片)的显示。

具体页面不需要重复编写这些重复的模块。

并且,这些模块需要调整时,只需要修改一个地方,便可以在所有地方生效。

2. 继承

能够编写一些基础模版,定义常见的页面结构。

具体页面继承这些基础模版,便不需要重复编写那些结构代码。

同样的,当页面结构需要调整时,也是修改一处,所有生效。

3. i18n

网页模版的国际化支持是一个模版引擎是否成熟的表现。

如果没有,当网站需要同时提供多种不同语言支持的时候,会很麻烦。

成熟的模版,都会提供内置的支持。

因为网页模版实现实在是太多了,大家功能也都差不多,那么性能,也就成为了相当重要的比较指标。

有的模版,能够“编译“,渲染起来快些。

一般可以简单认为,功能越多的模版,性能会约低。有的模版,甚至将i18n的支持变成可配置的,不需要的时候就可以关闭,以提高性能。

也有的模版认为,写 {% %} <%%> {{}} 这样的符号太麻烦了,可以直接忽略,它可以自动聪明的识别 html,以及模版控制代码。简单的说,就是以极其华丽的方式,去方面程序员少打几个字符。

还有的模版,在实现嵌入功能的时候,还可以选择所依赖的的css / js文件。

比方说,要显示用户的名片,需要引入 namecard.css;那么,可以在 namecard的模块文件中指定这个依赖,然后模版渲染的时候,自动把这个css的引用,放在html的头部。

直接在模块文件中写 namecard.css 的引用是很傻的,因为模块可以在模版中引用多次。重复引用同一个css文件是没有道理的。

种种模版功能细节,实际上,都是可以在没有模版支持的框架中去实现。

想想PHP,它本来是非常简单的,默认只能够在同一个文件中混杂逻辑与代码。

但一旦程序员有了追求,它也可以有模版实现。

模版不支持 i18n,程序员一般也是有办法在现有模版实现中添加相应的支持的。

并不复杂,关键是看程序员的态度;看程序员是否有把事情做得更好、更优雅的态度。

一般情况下,程序员选择去实现更多的模版功能的时候,必须先看看别人是怎么做的。比方说,如果完全不知道什么是gettext就去自行实现模版的 i18n 功能,是非常2B的。

绝大多数情况下,程序员面临的问题,都不是自己独有的,一定是别人已经解决过的问题。

是否有足够的见识,有足够的知识广度,了解别人的解决同样问题的做法是程序员能力的表现。

今天只讲C。

传统上,php / asp / asp.net web form等,使用的是所谓的 Page Controller Patterns:http://martinfowler.com/eaaCatalog/pageController.html

Page Controller简单的说,便是一个网址对应一个程序文件。

所以,我们会看到大量类似: show.php / show.asp / show.aspx 的网址存在,这样的网址,背后都有相应同名的文件。

这样的模式,是网站从静态转向动态是最自然的改变方便,也最为容易让初学者接受。

但随着网站的复杂化,这样的模式会慢慢显得不够方便;比方说,多个不同的网址,映射到相同的处理;比方说,处理的时候,复用共同的资源。

页面内容的动态化,同一个程序文件,显示的内容是动态生成的 - 根据不同的query string,生成不同的内容,如:show.php?id=1234

网页程序内部,实际上是需要解析网址中的query string,并做不同的操作。

这实际上是一个映射的过程,将网址映射到相应的处理。

为了方便做这样的映射,慢慢的出现了所谓的 Front Controller Patterns:  http://martinfowler.com/eaaCatalog/frontController.html

这是通过某种机制,将符合各种规则的网址请求映射到程序中的一个类,或者是一个函数处理。

一般上,是使用正则表达式解析网址,并映射。

将网址映射到一个类;

  1. urls = ("/home""hello")  
  2. app = web.application(urls, globals())  
  3.  
  4. class hello:  
  5.   def GET(self):  
  6.     return 'Hello, world!' 

将网址请求映射到类,是相对较“重”的处理方式,比方说,需要处理类的初始化等等。

有的框架,也可以是一个函数,则相对“轻量”一些:

  1. (r'^$''home'),  
  2.  
  3. def home(request):  
  4.   return HttpResponse("Hello, world."

类、函数,均各有优劣,但实际差异很小:

映射到类的方式,往往还会根据不同的HTTP header映射到类里面中相映的函数,比方说,将对 /home 的HTTP GET请求映射给 hello 类的 GET 函数;而对 /home 的 HTTP POST请求映射给 hello 类的POST函数。

这部分 url routing的设计与实现,各种语言、平台上的功能均向正则表达式靠拢,大同小异。

有的可能专门为 restful 做了优化,但即便木有,自行实现也并不复杂。

很多请求,都会有一些常用的默认处理,比方说,检查用户是否登陆,检查用户是否有权限等等。

这些业务控制逻辑,是完全可以复用的。

在Page Controller的场景下,一般是通过继承来实现;而Front Controller场景下,而一般通过函数修饰符的风格实现,如:

  1. class UploadImgHandler(BaseHandler):  
  2.  @tornado.web.authenticated  
  3.  def post(self):  
  4.   XXX 

(上述代码,实际上既使用了继承,也使用了修饰符。)

Controller的改进,目的在于更加方便的维护代码、修改业务逻辑。

如果程序员有良好的开发风格,基本是使用最基础的php page controller,也可以达到类似的效果。

各种“先进框架”,实际上是将常用的模式抽象出来,并通过便利的约定方式向程序员开放;如果程序员缺乏维护代码的意识,也很可能将良好的约定习惯用滥。

需要了解的,是为什么各框架的controller设计会有这样的设计,并用好;而不是死板的遵循“开发指南”。

在简单业务场景下,实际上page controller会更加方便。

有这么一个“定理”:概念越简单的模式,在处理简单场景时,是越便利;但随着场景复杂化,简单的模式会越来越难以维护。

而概念相对复杂、高级的模式,处理简单场景时,会相对麻烦;但随着场景复杂化,则比简单的模式容易维护。

“复杂度是守恒”的:
  模式简单,维护则复杂。
  模式复杂,维护则简单。

一个复杂的地方变简单了,则另一个地方会变复杂;保持代码结构的清晰,不要自己给自己添麻烦。

什么叫自己给自己添麻烦?

普通复数形式,加s: pigs / cats / dogs

已经可以很好了,但偏生有人要增加不规则复数:

sheep / mice / wives

这种就是自己给自己添麻烦。

这次来讲MVC中最后的M。

Model,几乎可以说是网页应用的核心。

之前课程提到过网页应用是由数据库驱动,而在很多场景,数据库 = M ; M = 数据库。

所谓的ORM; object relational mapping。

现在新的网页开发框架,特别是MVC框架,都会提供ORM支持,避免程序员直接写SQL、操作数据库。

传统上,ASP/ php臭名昭著的sql注入问题,便是因为菜鸟程序员直接在程序中根据用户输入拼接数据库造成的;而使用ORM框架,则可以彻底避免这种问题。

ORM有两种风格,一种是 R => O;一种是 O => R 。

====== R => O ======

传统上,程序员也都是先完成数据库设计(甚至是由DBA完成),然后再考虑相应的对象生成,也就是所谓的 R => O。

在这样的场景下,整个软件的框架,还是以数据库为核心,业务的设计思维是以关系型数据库的表结构为基础去考虑的,具体应用实现上,会考虑很多关系型数据库的功能特性,比方说,外键,joining等等,并且,程序员需要直接考虑“数据库设计三范式”,以及冗余字段等面向数据库的优化手段。

并且,程序员也很可能会采用数据库的一些高级特性,如视图、存储过程、甚至触发器等等以方便使用。

O的存在,仅仅是为了方便操作数据库表。

====== O => R ======

这种设计哲学则是相反,程序员做业务分析、实现架构设计的时候,并不过多考虑数据库的特性与限制;程序仅考虑自己的业务对象:编程语言中的对象。

数据库仅仅只是作为一个对象持久层来考虑:

* 程序运行的时候,对象是自动保持在内存中。

* 但在对象状态改变、程序退出的时候,将对象保存进数据库。

* 程序重新启动的时候,则从数据库中获得原先数据,并还原为内存中的对象。

在这样的场景下,数据库是一个可以被替换的存储层,它可以是关系型数据库,也可以是NoSQL,甚至是硬盘文件;所以,即便使用关系型数据库,一般也不会使用其高级功能。

设计哲学的不同直接造成了使用技术的不同。

====== 比较 ======

在ED开发圣经PEAA中,列举了下面三种方案:

1. Trascation Script

也就是直接拼接SQL啦~

2. Table Module

R => O;并且,O非常简单,直接以类似数组的方式读取表数据。

.net中ADO.net的DataTable / DataRow对象便是这种设计的典型实现。

3. Domain Model

O => R,直接设计业务领域(Domain)的对象,然后在考虑对象的持久化方案。

针对上面3中方案,Martin Fowler画了下面这张著名图解:http://www.hamishgraham.net/page/Work-Habits.aspx/Architecture/Business-Layer

他的结论是:

使用Table Module的方式,永远比直接写SQL简单;在简单的业务场景下,Table Module也会比Domain Model简单,但Table Module的方案复杂度会随着业务复杂化而快速增长。

反之,Domain Model的复杂度跟业务复杂度相比始终保持水平增长;它虽然一开始最复杂,但随着业务复杂度超过一定程度后,它反而会成为最简单的方案。

就我自己的开发经验,基本与Fowler的描述吻合;但随着ORM技术的成熟,Domain Model,未必如他在图中画的那样,一开始就有那么高的复杂度。

关键是看程序员是否习惯于关系型数据库的实现方案,如果是,那么,切换去Domain Model,确实会比较麻烦,各种不适应。

但如果是一个没有关系型数据库经验的程序员,或者说,没有强制使用SQL思维习惯的程序员,使用Domain Model,也可以是很自然的方案。

课程中提到PEAA中只有一页半的Active Record Pattern ( http://martinfowler.com/eaaCatalog/activeRecord.html )影响了过去5年多6年的Web开发潮流。

这个潮流是由Ruby On Rails引领的。

RoR的作者DHH David Heinemeier Hansson是Hacker,他因为RoR在2005年被Google跟O'Reilly选为年度黑客。

他在设计RoR时,选用了Active Record作为RoR的M层。

Active Record非常简单,一个类对应一个表,一个对象实例对应一行数据;并且有简单的有Save / Delete以及查询等简单的函数操作。

严格的说,Active Record不是福勒所推崇的充血Domain Object模型 ( http://martinfowler.com/bliki/AnemicDomainModel.html ),Active Record对象提供的功能函数太少,只有通用的数据操作方法,而不包涵业务逻辑;但它又不像POJO ( http://martinfowler.com/bliki/POJO.html ) 那样完全的贫血。

(充血、贫血Domain Object之争,可以去iteye翻帖子)

从福勒 AnemicDomainModel 一文看,他在当年(2003)是推荐了充血Domain对象跟POJO,但过去几年在Web开发领域所流行的却是 Active Record这样两边都沾点,但却又不全是的中间妥协方案。

不搞教条主义,什么实用用什么,POJO不够,那么就加一点;充血太复杂,那么就减少一点。

从互联网的发展看,我一时间完全想不出有什么在理论上被设计得很好的模型,能够最终经历时间考验成为事实标准。

因特网的7层模型,实际用到的远不到7层;Java的EJB挂了;XML被JSON取代等等等等。

也许学院派提出的理论有他们的应用场景,只是,这样的场景,在快速发展互联网似乎很难找到例子。

互联网产品的业务相对简单,Active Record已经足够好,足够方便,因此大行其道。

另一方面,互联网产品做大后,也往往有着极大的性能要求,一个复杂的模型,是难以做性能优化的。

像Active Record,因为足够简单,Twitter在当年遇到性能问题的时候,便直接Hack掉RoR的实现,增加了 Cache-Money ( https://github.com/nkallen/cache-money ) 这么一个透明的缓存层。

如果RoR使用的是充血对象模型,对象中有复杂的业务逻辑,如何增加透明的缓存呢?

Active Record的实际上是对数据库操作做了抽象。

封装、抽象是一门艺术。

什么该封装,什么该暴露,什么彻底不可见,需要拿捏得很准确。

最容易犯的错误是过度封装,使得一些本来很简单的底层操作,到了上层变得完全不能用;或者说,很难用。

开发者需要用到hack的方式,才能去做这些简单的操作。

Active Record便是一个抽象封装得恰到好处的例子。过度设计、过度封装的数据操作层?EJB。

按照教科书对OO的定义,OO的核心特性之一是:encapsulation http://en.wikipedia.org/wiki/Encapsulation_%28object-oriented_programming%29

Private属性、方法,对象外部是完全不能访问的。

但如果遇到了需要访问的场景怎么办?!

有的人会说:“这样的场景本来就不应该出现,这是对象设计一开始没有做好造成的,错误的应该设成Public的属性设成了Private”。

ORM,采用O => R的映射的设计哲学,只考虑业务对象,完全不考虑底层数据库,数据库仅仅是一个可以被替换掉的持久层,它可以是关系型数据库、也可以是NoSQL,甚至是硬盘文件。

也就是说,Domain Object是把后端数据库给设成“Private”了,即便底层是关系型数据库,你也不可以直接去写SQL。

即便你使用的是MS SQL Server,你也不能去调用它特有的SQL特性。

Asp.Net刚出来的时候,微软曾经鼓吹过一个叫 N-Tiers 的架构:http://msdn.microsoft.com/en-us/library/ms973279.aspx / http://msdn.microsoft.com/en-us/library/bb384398.aspx

我曾经以为这是王道,直到我膝盖中了一箭……呃,不,直到我看了Joel Spolsky写的 The Law of Leaky Abstractions:http://www.joelonsoftware.com/articles/LeakyAbstractions.html

理想很丰满,现实很骨感。

ORM工具再怎么封装都好,底层用了数据库,就是用了数据库。

开发者必然需要了解数据库的特性,能否直接调用数据库的特性,是一个选择。

是否要彻底对上层屏蔽掉数据库的存在,也是一个选择。

N-tiers架构推荐一层又一层的封装,如果错误使用,把选择当成教条,是会有噩梦的。

========

Python是一门很有趣的语言,它支持继承,能实现OO,但是缺乏 encapsulation 的语言支持。

Python根本就没有public / private这样的关键字,然后呢?

然后可以回过头再去看:“这样的场景本来就不应该出现,这是对象设计一开始没有做好造成的,错误的应该设成Public的属性设成了Private”。

抱歉!评论已关闭.