后端实现的主要考虑
当api设计确定以后,剩下的工作就是实现了。我跳过架构和设计这两个环节,因为用了Rails框架之后,这两个环节的工作简而又简。加之项目本身在前期并不庞大,还不需要过多的设计。
首先介绍一下Rails框架的基本结构吧。Rails框架是一款用Ruby开发的后端MVC框架,由于我只是开发API应用,其中的View层就被我无端地简化了。我的重点工作只剩下定义Model层和实现Controller层即可,最终是在Controller层返回JSON数据,没有显示地定义视图。一个Rails应用的目录结构如下:
app/ - 应用主目录
controllers/ - 控制器所在目录
models/ - 模型所在目录
config/ - 配置所在目录
routes.rb - 路由配置文件
mongoid.rb - MongoDB数据库配置文件
其实还有其他的目录,只是我没说而已。Rails框架很自然地为你规定好项目的基本结构,我剩下的工作就是往里面填自己的代码。这一过程大体是:
1. 在app/models下面定义自己的Model(我之前说过,我用的是Mongoid为MongoDB数据库的collection定义模式)
2. 在app/controlles下面定义自己的Controller。这里面,Controller的任何方法都可以配置为API接口的处理方法。Controller的方法大致就是处理前端的API请求逻辑,有时不可避免地要调用Model层的一些东西。
3. 在routes.rb中配置API与Controller中的方法的映射关系。
不过,虽说是填代码,但还是要有很多要考虑的地方。
DRY:Don't Repeat Yourself
我把它视为首要的考虑。如果项目中存在重复代码,就可以看成存在设计缺陷。
一个典型的例子是这样的:我之前说过,我的API设计中存在分页(from,size),sort,include,with_total,just_total等控制,其中from,size,sort是Model查询时的控制参数,include,with_total, just_total是作为返回JSON数据时的控制参数。我的所有List接口,即形如GET /users这样的接口都是有这些控制参数的。这其中要进行如下处理:
1 from, size = 合适的值,用于分页 2 sort = 将params[:sort]转化成能被Model识别的形式 3 cond = 将params[:cond]转化为能被Model识别的形式 4 users = User.where(cond).order(sort).limit(size).offset(from) 5 includes = 将params[:include]转化为能够被as_json识别的形式 6 list = 基本就是users.as_json(includes), 但还要处理with_total,just_total的情况 7 render json: list
基本来说,前3行代码和后3行代码会在其他诸如GET /tickets这样的接口的处理方法中调用,它们就是重复代码。不能像这样一个一个都重复写到每一个方法中去,要把它们提取到公共的方法中来。我采取的做法是,把前三行代码封装到一个叫做query_ready的方法中去,后3行代码提取到名为respond的方法中去,这两个方法都被定义在ApplicationController中。由于项目中的每一个Controller都继承自ApplicationController,所以可以直接调用这两个方法。如上,上面的7行调用就转化成下面的形式:
respond query_ready(User)
对于处理GET /tickets的接口,可以实现成
respond query_ready(Ticket)
单纯的代码 - 把异常分离
这里主要介绍的是Rails的拦截器技巧。我的想法是,Controller中的每一个处理方法都尽量只考虑正常的情况,而把异常分离到其他的回调代码中去。一个很简单的例子:
/tickets/:id/refund 用户退票的接口。用户退票,首先需要是一个已登录的用户;其次,这个已登录用户持有该票,因为你不能修改他人的票的状态。一个典型的实现如下:
if not login: render_error '未登录' else ticket = Ticket.find(params[:id]) if ticket.owner != login_user render_error '无权限' else ticket.state = :refund render_success 'OK'
上面的代码杂糅了正常的逻辑和出错的逻辑,我在其中做了分离。出错的逻辑分离到两个方法分别是require_login和require_permission中去,然后用before_action回调进行控制。大致就是下面的样子了:
#这个方法和下一个方法可以上升到ApplicationController中去,从而让所有Controller都能用 def require_login: if user_not_login render status: 401, json: {error: '未登录'} end end def require_permission: if not resource.permit?(login_user) render status: 403, json: {error: '无权限'} end end before_action :require_login before_action :require_permission def refund Ticket.find(params[:id]).refund = :refund render json: {success: 'OK'} end
在Rails中before_action的逻辑是这样的,首先执行before_action声明的逻辑(即require_login和require_permission),如果其中render了status非2xx系列(可能也包含3xx系列,不详)的状态码,就不会执行正常的逻辑(即refund)。
这样做的好处是代码结构更加清晰,如今refund只需要考虑正常情况下的处理逻辑即可,简短的两行代码清晰异常。更为重要的是,它遵循了DRY原则。因为未登录和无权限两项异常是很多接口都要处理的。
在上面的处理中,还有个小插曲,反应了Rails中元编程的便利性。在上面的resource.permit?(login_user)的这行代码中,permit?方法倒还好,在每个Model中直接定义就好。但是resource方法就有很多技巧可言了。在接口/users/:id当中就应该是User.find(:id),在/tickets/:id中就应该是Ticket.find(:id),这还要包括诸如/tickets/:id/refund, /users/:user_id/tickets等情形。同时处理这么多种情况,就要充分利用Rails提供的元编程特性,这里主要涉及到单复数转换,帕斯卡命名法向驼峰式命名法的转换以及Ruby类其实就是常量这些技巧。我把实际项目中的代码贴在下方。
def resource
return @resource if @resource
path_param_names = request.path_parameters.keys
if path_param_names.include? :id
id_param_name = :id
resource_name = params[:controller].singularize
else
id_param_name = path_param_names.find {|name| name.to_s.end_with? 'id'}
resource_name = id_param_name.to_s.sub(/_id$/, '')
end
model_class = resource_name.camelize.constantize
id = params[id_param_name]
@resource = model_class.find(id)
end
另一个异常拦截是使用rescue_from. 这是一个真正的异常拦截器,它拦截的是真正的Ruby异常对象。一个典型的例子是访问/tickets/123的时候找不到id为123的Ticket。这时应该抛出404Error。原本的逻辑是:
def show_ticket begin ticket = Ticket.find(params[:id]) render json: ticket rescue Mongoid::Errors::DocumentNotFound render status: 404, json: {error: '您访问的资源不存在'} end end
实际中我做了分离,像下面:
#可以提到ApplicationController中以让所有Controller都可以用到 rescue_from Mongoid::Errors::DocumentNotFound do render status: 404, json: {error: '您访问的资源不存在'} end def show_ticket ticket = Ticket.find(params[:id]) render json: ticket #只用考虑正常的逻辑即可 end
我在项目中很多地方使用before_action, rescue_from做了类似的处理。before_action, after_action, round_action和rescue_from联合构成了Rails的切面编程体系。