也许后端MVC的说法已经过时了
呃,标题有点耸人听闻,不过我并不是标题党。考虑到谈论大而虚的东西(比如最好的语言)容易引起争论,所以还请诸君带着看戏而不是庭辩的心态来看待本文。
依我个人所见,后端框架,类似于MVC这样的组织方式已经显得过气了。
过去,在创建应用时通常会按MVC各建一个文件夹,每个文件夹就是一个模块。MVC三者的职责是这样的:
Controller绑定到某个路由上,接着处理请求参数,然后创建在整个请求中可见的对象,并进行一些业务逻辑上的工作。期间会从数据库中构造Model,也有可能新建/修改Model后将它们保存入数据库。最后,Controller会通过View响应用户的请求,或者返回重定向的报文。基本上业务逻辑都是实现在Controller里面的。(下文为了阐述方便,都以“业务逻辑”特指Controller中的业务逻辑)
每个Model往往对应数据库的一个实体。Model类除了装数据之外,还提供了跟自己身份相关的一些方法,比如
User
类提供authenticate
方法以用于验证密码。Model对象通常还会负责检验构造自己的参数是否正确。View负责使用给定的参数渲染模板,并作为响应返回给用户。View一般是由该框架提供的模板语言写成。
现在,一个后端应用如果还是按MVC的方式划分,似乎有些不适应了。
后端MVC的历史
我们先从后端MVC的历史开始讲起吧。MVC最初发端于客户端程序的开发,旨在用Controller在Model和View之间进行解耦。如果我没记错的话,gang of four的《设计模式》里面提到MVC,就是以客户端程序作为例子。
跟客户端开发类似的,后端程序一开始也只有View这一层。在很久很久以前,后端程序都是这样运行的:用户一个HTTP请求进来,服务器通过cgi调用一个程序生成文本作为响应。这时期大部分后端程序,看上去都像是模板语言(见过初学者写过的JSP/PHP吗?)。因为它们主要做的事情,就是从用户输入和数据库中获取数据,并拼接字符串生成文本。后来后端程序开始演化得越来越复杂,单单一层View已经不适应了。由于需要把逻辑从View中分割开来,后端程序开始走上客户端程序走过的路,进行MVC的分离。于是,负责路由和业务逻辑处理的部分变成了Controller,负责数据处理和持久化的部分变成了Model。
虽然后端程序也是做了MVC的拆分,但是它跟客户端的MVC其实是不同的。
在客户端里,Controller把View和Model分离开来,实现View和Model的解耦合。View上的变化,通过Controller传递给Model,然后再将Model最新的数据通过Controller传递回View。View:Controller:Model的比例通常是N:1:N,其中每个View基本对应一个Model。
然而,在后端程序里面,View和Model通常没有很强的对应关系。一般意义上的CRUD,基本上是Controller(业务逻辑)围绕着Model(数据层)在转。View扮演的往往是跑龙套的角色。
还需要V层吗
在客户端MVC中,View扮演的是跟其他二者三足鼎立的角色。用户的输入经过View,底层数据的变更通过View反馈给用户。
然而后端MVC中,View的地位摇摇欲坠、可有可无。前文提到,Controller绑定在路由上,接收请求;Controller渲染模板,发送响应。跟客户端不同的是,Controller只有在渲染模板时才用上View。View的戏份一下子被砍掉了一半。祸不单行,Controller并不一定需要渲染模板来发送响应,它可能直接就重定向了;或者更常见的是,Controller直接把一个对象JSON化,并把它响应给用户。这么一来,View的戏份还剩多少?
有些后端框架提供了JSON格式的模板,多多少少试图挽救View的没落地位。可惜并没有什么用。
过去,View通常由三部分组成:html代码,控制流程语句,渲染时的上下文。如果提供的是JSON格式的模板,那么View的前两部分基本不需要了,只需要渲染时的上下文。可是这么一来,为什么我还需要渲染一个JSON模板,直接用上下文的数据创建出个实例,由它生成JSON字符串,不也行吗?虽然我多了个用于响应的类,但是少了个JSON模板啊,而且说不定就不用给View留个文件夹。
最近我参与开发的几个后端应用,根本没有View的容身之地。所有的响应都是JSON格式,都是由特定的类JSON化出来的。
很多情况下,后端应用要么仅仅是大后端系统中的一个组件,要么需要跟多种来源的客户端打交道。通常它们只是作为数据的守护者,API的执行人,仅响应以JSON数据。至于接收者想用这些数据做什么,那是它们的事了。也许是拿来渲染前端页面,也许是保存在客户端的数据库里,也许是拿去进一步分析处理。View已经退化到算不上一个层了。
M负责数据实体还是负责数据的访问
说完摇摇欲坠的View,接着说地位尴尬的Model。Model是数据的化身,后端开发千变万变,核心都是数据的处理。可以说,Model就是占了个风水宝位。不过在我看来,当前常见的做法——只划分一个Model包——并不够清晰。
以我愚见,后端程序中的Model其实做了两件事。一件事是表示了数据实体,另一件则是负责数据的访问。按照单一职责原则,Model这样一身饰两角是不对的。数据实体是一回事,对应的数据实体的访问是另一件事,两者不能混起来。
假设保存Account
需要一个事务,在这个事务里面要更新Account
和Balance
两个实体。下面是Rails里面的做法:
# always save Account in a transaction Account.transaction do balance.save! account.save! end
问题是,这段代码应该放到哪里?一个做法是放到Controller里面,但是保存Account
的方式,不应该放到Account
里面吗?另一个做法是放到Account
类里面,但是为什么不放到Balance
里面呢,这个事务也保存了Balance
。作为程序员,在这件事上可不能偏心哦。
如果提供了DAO作为中间层,那么就不会这种“偏心”的顾忌了。而且这种带事务的保存,跟Account
类自带的save
方法的差异,也从层级上体现出来。
此外,Model层里面的类,不一定对应着数据库上的表。每个Model都知道如何持久化自身数据,这种假定是无法一直保持下去的。如果没有把数据实体和访问数据实体的组件区分开来,总有一天会陷入名不符实的危机中。
一个好的例子是,SQLAlchemy
提供了Session
类来完成对具体数据(Model)的访问操作(事务、保存等等),这样仅需稍加包装,我们就能分离出一个数据访问层出来,避免数据实体和数据访问间纠缠不清。
session = Session() try: account = session.query(Account).get(...) balance = session.query(Balance).get(...) ... # 对account和balance做些修改 session.commit() except: session.rollback()
C:什么都往里装
调侃了View和Model,是时候对最后的Controller下手了。相对于View负责展示,Model负责数据,Controller的职责并不清晰。Controller是个筐,什么都可以往里面装。凡是无法区分到View和Model的,都放到Controller里面吧。所以,在MVC中,Controller往往是最臃肿的。
终于有一天,我们下定决心要整治下Controller乱七八糟的环境。一个通常的做法是,把某个路由上Controller的函数,拆分成若干个小函数。这些小函数不绑定路由,纯粹就是业务逻辑的抽象。拆分之后,Controller不再臃肿了,抽象出来的业务逻辑也可以被复用。
其实往更深一点思考,也许Controller本来就可以拆成两部分,一部分负责绑定路由,另一部分负责业务逻辑。
绑定路由的部分,负责解决请求数据的完整性和正确性,及限流、鉴权等操作。在它的眼里,看到的是HTTP报文。
业务逻辑的部分,负责具体业务处理。在它的眼里,看到的是用户的操作。
这样一来,业务逻辑的实现就跟路由绑定解耦合。我们可以给不同的路由提供一样的业务逻辑处理的同时,保持在限流等方面上的区别对待。我们也可以以此解决API设计上的遗留问题——旧的API,就让它们调用到新的业务逻辑上。
新的划分方式
View消亡了
Model分离成两层,一层负责数据实体,另一层负责数据的访问。
Controller分离成两层,一层负责绑定路由,另一层负责业务逻辑。