你的位置:首页 > ASP.net教程

[ASP.net教程]后端实现的主要考虑


当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: '未登录'}  endenddef require_permission:  if not resource.permit?(login_user)    render status: 403, json: {error: '无权限'}  endendbefore_action :require_loginbefore_action :require_permissiondef 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  if(@resource) then return @resource end  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: '您访问的资源不存在'}  endend

实际中我做了分离,像下面:

#可以提到ApplicationController中以让所有Controller都可以用到rescue_from Mongoid::Errors::DocumentNotFound do  render status: 404, json: {error: '您访问的资源不存在'}enddef show_ticket  ticket = Ticket.find(params[:id])  render json: ticket #只用考虑正常的逻辑即可end

我在项目中很多地方使用before_action, rescue_from做了类似的处理。before_action, after_action, round_action和rescue_from联合构成了Rails的切面编程体系。