Ruby 用 method chain 方式来包装 HTTP API 调用

IChou · May 07, 2017 · Last by MaskRay replied at December 09, 2017 · 4174 hits

最近项目组有一个关于人脸识别相关的需求,需要用到 FacePP 的服务,而官方提供的 Ruby SDK 自 2013 年提交以来,从未更新过,Issue 和 PR 也无人处理。So, 我被安排去『重新』实现 FacePP 的 SDK。

官方 SDK 地址:https://github.com/FacePlusPlus/facepp-ruby-sdk

本以为这是个苦差事,难鹅,当我拉下官方的 SDK 源码时,却被华丽丽的惊艳到了 w(°o°)w

通常来说我们写 SDK 的常规思路:

  1. 实现一个通用的请求处理,包括 url 拼装,参数处理,签名等
  2. 根据各个接口去实现一个方法(method)

而这个 SDK 的实现,直接使用了 Ruby 里的大杀器 —— 元编程,进而毫不费力的实现了链式调用

链式调用

关于链式调用相信大家都不陌生,最常见的就是 ActiveRecord 的查询方法

Article.where('id > 10').limit(20).order('id desc').only(:order, :where)

个人认为,链式调用最大的优点就是优雅,相比起把所有参数放在一个 options(hash) 里喂给一个方法的调用方式,链式调用的可读性明显更好,参数组合也更自由。

FacePP 的 SDK 里面实现的链式调用

api = FacePP.new 'YOUR_API_KEY', 'YOUR_API_SECRET'
puts api.detection.detect url: '/tmp/0.jpg'

实现原理

链式调用的实现原理其实很好理解,每当你调用一个链式对象的某个方法时,返回一个该对象所属类的新实例即可

比如在 ActiveRecord 中,当你调用 Model Articlewhere 方法时,它返回了一个 ActiveRecord::Relation 实例,假定它叫 relation_1.limit(20) 其实调用的是relation_1limit 方法,然后返回一个新的 ActiveRecord::Relation 实例 relation_2,以此类推。

所以当你只是调用 ActiveRecord::Relation 的各种查询方法时,并没有真的触发查询,而是不停的返回新的 ActiveRecord::Relation 实例,直到遇到第一个需要取值的调用,才会触发查询,并返回数据。

以上只是简单的描述,实际上 ActiveRecord::Relation 的实现还挺复杂的,有兴趣可以去看看源码:

https://github.com/rails/rails/blob/5-1-stable/activerecord/lib/active_record/relation.rb

相比起数据库查询的复杂性,http api 的复杂度就算很低了,因此在 http 接口上实现链式调用,其实可以很容易。

简单实现

这个部分我就直接贴 FacePP SDK 的源码了:

# https://github.com/FacePlusPlus/facepp-ruby-sdk/blob/master/lib/facepp/client.rb
# 代码略有删减

class FacePP
  APIS = [
      '/detection/detect',
      '/info/get_image',
      # ...
    ]

  def initialize(key, secret, options={})
    APIS.each do |api|
      m = self
      breadcrumbs = api.split('/')[1..-1]
      breadcrumbs[0..-2].each do |breadcrumb|
        unless m.instance_variable_defined? "@#{breadcrumb}"
          m.instance_variable_set "@#{breadcrumb}", Object.new
          m.singleton_class.class_eval do
            attr_reader breadcrumb
          end
        end
        m = m.instance_variable_get "@#{breadcrumb}"
      end

      m.define_singleton_method breadcrumbs[-1] do |*args|
        # send a request to #{api} with #{args}
      end
    end
  end
end
  1. 先预置了一个 api path 的列表,相当于一个路由表。
  2. 当 FacePP 被 new 的时候,会逐条解析这个路由表,把每条路由以 / 作分割符解析为数组。
  3. 遍历数组至倒数第二个元素,把每个元素变成上层对象的一个同名实例变量,其值是一个新的 Object 实例,并通过 attr_reader 为该实例变量添加访问方法
  4. 将数组的最后一个元素变成上层对象的一个 singleton_method, 里面包含了真正的请求代码。

其成果就是,我们可以以

api.detection.detect url: '/tmp/0.jpg'

这样的方式,『形象的』调用 FacePP 的各个接口。当有新增接口的时候,也只需要添加一条路由即可。

作者 @MaskRay 用一个普通的 Object 替代了 ActiveRecord::Relation 的功能,我觉得是一种灰常 geek 的方式。因为这个东西足够简单,我们并没有必要去造一个自己的 Relation

改进空间

假定我们的需求场景再复杂一点

  1. 包含的项目多,接口数量庞大,接口变动相对频繁
  2. 常用的 4 种 http 请求方式都需要被支持(FacePP 所有接口都是 POST)
  3. 被调用的路由很长,但前面有一大段是几乎不会变的前缀
  4. 各个接口的的请求实现方式可能不完全一样

由此,我想到了一些改进思路

  1. 抛弃预置路由表,通过覆写 method_missing 方法,在被调用的时候才去生成链式对象
  2. get|post|put|deleteindex|show|create|update|destroy|save 作为最后一层发起请求的方法来结束一串调用
  3. 为链式对象 Object.new 增加一些实例变量,比如 @host@path 等,初始化时可以通过附加参数指定前缀等参数
  4. 允许传入一个 block

总结

在我所在的公司,有一个内部 gem 叫 services_support, 专门用来处理系统间的 api 调用。这个 gem 实现了两种接口调用方式:

  • 一种是诸如 ServicesSupport::BMS.post 'api/orders', args 这样将 path 作为参数传入
  • 一种是预定义一个 ServicesSupport::BMS.create_order(args) 方法来调用

实际使用中,几乎所有同事都倾向于使用后面这种方式来书写代码,有定义好的要用,没有定义好的自己去加上也要用。不知道这是不是 Rubyists 们追求代码优雅的一个常态。

Anyway,等我用链式调用重写了这个 gem 后,他们就再也不用纠结怎么调了,也不用在新增接口时一个个的去新增调用方法了。想一想那酸爽,鸡肉味,嘎嘣脆~~~

PS:最后还是贴一下我更新之后的 FacePP SDK 吧,没准有人需要 😝

https://github.com/IvanChou/facepp-ruby-sdk

居然没人回复,唉。。国内 ruby 的没落

类似这样?

ruby_books = Request.header(token: token).url('books').params(name: 'ruby').get
Reply to zfjoy520

公司打算弃用 rails 只保留 ruby 相关的脚本,哎!!!

Reply to adamshen

okhttp?

说起链式调用 java 和 c#才是鼻祖。。。

LZ 可以把标题改成 用 method chain 来 wrap API 之类的。

Reply to adamshen

这样也极好的

我之前想到的是 如果请求 ServiceA 的 /api/orders 接口

ServiceA.api.orders.index name: 'ruby'
ServiceA.api.orders.create name: 'rails', version: '5.0.1'

.get 这种主要是用来对付对面服务不是严格 Restful 的情况

Reply to gyorou

感谢建议

我们公司也开始转语言了,唉~~~

Reply to IChou

你反正是万金油,无所谓!

Reply to IChou

你们也要转语言....不会吧....听说你们要搬到西门那边了?

都转啥语言了?

Reply to Catherine

已经搬了 每天少睡 1 个多小时 我已经连续一周睡眠不足了

Reply to freeman

你猜 😂

Reply to IChou

我每天从你们现在位置到南门上班😅 😅

我猜一下,转成 java 了...

Reply to Catherine

猜中了

我们公司也在做人脸识别什么的不知道有什么用,我先收藏下来,ruby 也可以做人脸指纹识别吗

Reply to nicetyler

我们只是正好有这个业务需求,直接用的 FacePP 的服务,并没有自己去搞算法什么的

我只是来顶贴的,赞一个~

好多年前實習幹得不舒服的時候發現想玩點稍微正常點的東西隨便寫的,都被您拖出來……

You need to Sign in before reply, if you don't have an account, please Sign up first.