zoukankan      html  css  js  c++  java
  • 后端实现的主要考虑

    当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_loginrequire_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的切面编程体系。

  • 相关阅读:
    君子藏器于身,伺时而动
    高级前端面试集锦
    ES6标准入门(第三版).pdf----推荐指数⭐⭐⭐⭐⭐
    《Vue.js实战》--推荐指数⭐⭐⭐⭐
    如何让vue自定义组件可以包裹内容,并且渲染出来,以及组件的组合使用
    vue里如何灵活的绑定class以及内联style
    为什么VUE注册组件命名时不能用大写的?
    在github上搭建一个静态的个人网站
    leetcode-6-basic
    leetcode-5-basic
  • 原文地址:https://www.cnblogs.com/starstone/p/4656187.html
Copyright © 2011-2022 走看看