Date

Strip 的滚动 API 设计经验

APIs as infrastructure: future-proofing Stripe with versioning

如果某个 API 返回一个名为 verifiedboolean 类型字段来表示账户状态,那么用户可能会编写出如下的代码

if bank_account[:verified]
  ...
else
  ...
end

但假如在后续迭代中将 verified 替换为包含 verifiedstatus 字段,那么便会影响用户之前所写的代码。这种类型的改变称为 "backwards-incompatible"。为了避免这种情况,之前存在的字段应该继续存在且保持相同的类型和名称。显然这对开发并不友好,但如果要用户更新他们的代码则又需要经过足够的协商

通常的方法是使用版本控制,v1v2v3 作为前缀添加在 URL 中或者 HTTP ACCEPT 首部中。OK 还行,但如果版本间变化太大会导致用户不愿意升级继续使用旧版本的 API。Stripe 使用滚动版本来实现版本控制,这些版本以发布日期来命名,比如 2017-05-24,携带在请求头 Stripe-Version 中。它们虽然也是 "backwards-incompatible" 的,但每个版本仅包含一小组改动,使得升级变得容易。这种方式有点类似于敏捷开发

但版本控制始终是改善开发人员体验和维护旧版本的额外负担之间的折衷。Stripe API 的每个响应都由 APIResource 类进行描述

class ChargeAPIResource
  required :id, String
  required :amount, Integer
end

当需要进行 "backwards-incompatible" 的更改时,将其封装在 version change module 中。此模块定义了关于此次更改的文档和转换以及修改的 API 资源类型

class CollapseEventRequest < AbstractVersionChange
  description \
    "Event objects (and webhooks) will now render " \
    "`request` subobject that contains a request ID " \
    "and idempotency key instead of just a string " \
    "request ID."

  response EventAPIResource do
    change :request, type_old: String, type_new: Hash

    run do |data|
      data.merge(:request => data[:request][:id])  # rollback
    end
  end
end

将这些 version change module 与 API 版本进行关联

class VersionChanges
  VERSIONS = {
    '2017-05-25' => [
      Change::AccountTypes,
      Change::CollapseEventRequest,
      Change::EventAccountToUserID
    ],
    '2017-04-06' => [Change::LegacyTransfers],
    '2017-02-14' => [
      Change::AutoexpandChargeDispute,
      Change::AutoexpandChargeRule
    ],
    '2017-01-27' => [Change::SourcedTransfersOnBts],
    ...
  }
end

当生成响应的时候,API 根据当前版本的 APIResource 来格式化数据,然后根据 Stripe-Version 来决定最终的版本,回溯并应用沿途找到 version change module,得到目标版本的 API 响应

这种方案的另一个优点是易于生成 changelog