Stripe 的常见设计模式

2025-05-28

Stripe 的常见设计模式

如果您还没有读过上一篇关于设计模式重要性的文章,我建议您先读一下,然后再读这篇文章。如果您已经对设计模式深信不疑,请继续阅读,了解我在 Stripe 中使用的一些最佳示例。

您可能不同意 Stripe API 的设计方式,最终的设计也可能与我们使用的有所不同。这没关系,因为不同的公司有不同的用例。因此,我在这里介绍一些我认为足够通用的设计模式,几乎适用于 API 设计过程中的任何人。

语言

命名并非易事。计算机科学中的大多数领域都是如此,API 设计也不例外。问题在于,与变量和函数命名类似,API 的路由、字段和类型也需要清晰简洁。即使在最佳情况下,这也很困难,但这里有一些建议。

使用简单的语言

这个建议显而易见,但实际操作起来却相当困难,这也是很多“bike-shedding”(自行车棚)现象的根源。尝试将概念提炼到核心,不要害怕使用同义词库来查找同义词。

例如,在构建平台时,不要混淆用户和客户的概念。用户(至少在 Stripe 的术语中)是直接使用平台产品的一方,而客户(也称为“最终用户”)是最终购买用户可能提供的商品或服务的一方。您不必使用完全相同的术语(“用户”和“最终用户”完全没问题),只要您的语言保持一致即可。

避免使用行话

每个行业都有自己的专业术语;不要想当然地认为你的用户对你所在行业了如指掌。例如,你信用卡上看到的 16 位数字称为主账号,简称 PAN。在金融科技圈,人们谈论 PAN、DPAN 和 FPAN 是很正常的事,所以在你的支付 API 中这样做也是情有可原的:



card.pan = 4242424242424242;


Enter fullscreen mode Exit fullscreen mode

即使您知道 PAN 的含义,它与信用卡上 16 位数字之间的联系可能仍然不太明显。请避免使用专业术语,使用更容易被更多人理解的术语:



card.number = 4242424242424242;


Enter fullscreen mode Exit fullscreen mode

在思考 API 的受众是谁时,这一点尤其重要。实现 API 的核心人员很可能是不懂金融科技或其他任何专业领域的开发人员。通常来说,最好假设人们不熟悉您所在行业的术语。

结构

优先使用枚举而不是布尔值

假设我们有一个用于订阅模型的 API。作为 API 的一部分,我们希望用户能够判断该订阅是否处于活动状态或已被取消。以下接口似乎是合理的:



Subscription.canceled={true, false}


Enter fullscreen mode Exit fullscreen mode

这种方式确实可行,但假设在实现上述功能一段时间后,我们决定推出一项新功能:暂停订阅。暂停订阅意味着我们暂停付款,但订阅仍然有效且未被取消。为了反映这种情况,我们可以考虑添加一个新字段:



Subscription.canceled={true, false}
Subscription.paused={true, false}


Enter fullscreen mode Exit fullscreen mode

现在,为了查看订阅的实际状态,我们需要查看两个字段,而不是一个。这也给我们带来了更多困惑:如果订阅有canceled: true和怎么办paused: true?已取消的订阅还能被暂停吗?或许我们可以认为这是一个 bug,并认为已取消的订阅必须有paused: false

这是否意味着已取消的订阅可以恢复?
添加的字段越多,问题就越严重。你无法检查单个值,而是需要一堆令人困惑的 if/else 语句才能弄清楚这个订阅到底发生了什么。

相反,让我们考虑以下模式:



Subscription.status={"active", "canceled"}


Enter fullscreen mode Exit fullscreen mode

通过使用枚举而不是布尔值,一个字段就能用简单的语言告诉我们对象的状态。这项技术的另一个好处是它带来了可扩展性和面向未来的特性。回到之前添加“暂停”机制的例子,我们只需要添加一个额外的枚举:



Subscription.status={"active", "canceled", "paused"}


Enter fullscreen mode Exit fullscreen mode

我们添加了功能,但保持了 API 的复杂度不变,同时更加清晰易懂。如果我们决定移除暂停订阅功能,移除枚举总是比移除字段更容易。

这并不意味着你永远不应该在 API 中使用布尔值,因为几乎可以肯定,在某些极端情况下,布尔值更有意义。相反,我建议你在添加布尔值之前,考虑一下未来布尔逻辑不再有意义的可能性(例如,有第三种选择)。

使用嵌套对象以实现未来的扩展

延续上一条建议:尝试按逻辑将字段分组。具体如下:



customer.address = {
  line1: "Main Street 123",
  city: "San Francisco",
  postal_code: "12345"
};


Enter fullscreen mode Exit fullscreen mode

比以下更干净:



customer.address_line1 = "Main street 123";
customer.address_city = "San Francisco";
customer.address_postal_code: "12345";


Enter fullscreen mode Exit fullscreen mode

第一种方案可以更轻松地添加其他字段(例如,country如果您决定将业务扩展到海外客户,则需要添加字段),并确保字段名称不会过长。保持资源顶层简洁明了不仅更可取,还能抚慰心灵。

回应

返回对象类型

大多数情况下,调用 API 是为了获取或修改某些数据。在后一种情况下,通常返回的是已修改资源的表示形式。例如,如果您更新了客户的电子邮件地址,那么作为 200 响应的一部分,您期望收到该客户更新后的电子邮件地址副本。

为了方便开发者,请明确说明返回的具体内容。在 Stripe API 中,object响应中有一个字段,可以清楚地说明我们正在处理的内容。例如,API 路由



/v1/customers/:customer/payment_methods/:payment_method


Enter fullscreen mode Exit fullscreen mode

返回与特定客户关联的 PaymentMethod。希望从路由中可以明显看出您应该返回的是 PaymentMethod,但为了以防万一,我们还是添加了该object字段,以确保不会造成混淆:



{
  "id": "pm_123",
  "object": "payment_method",
  "created": 1672217299,
  "customer": "cus_123",
  "livemode": false,
  ...
}


Enter fullscreen mode Exit fullscreen mode

这在筛选日志或向集成添加一些防御性编程时有很大帮助:



if (response.data.object !== 'payment_method') {
  // Not the expected object, bail
  return;
}


Enter fullscreen mode Exit fullscreen mode

安全

使用权限系统

假设您正在为产品仪表盘开发一项新功能,该功能是某位大客户特别要求的。您准备让他们测试该功能作为 Beta 版本以获取一些反馈,因此您需要告知他们应该使用哪个路由发送请求以及如何使用。新路由没有公开记录,除了您的客户之外,其他人应该都不知道,所以您不必太担心。

几周后,您推动了一项功能更改,解决了大客户给您的一些反馈,结果却收到其他用户的一系列愤怒的电子邮件,询问为什么他们的集成突然中断。

灾难来了,你的秘密 API 路由被泄露了。也许最初的客户对这个新功能兴奋不已,决定告诉他们的开发者朋友。又或许,客户的用户查看了他们的网络面板,看到了这些指向未记录 API 的请求,并觉得这个功能在他们自己的产品中看起来不错。

你不仅要清理当前的混乱局面,而且你的测试功能现在实际上已经被拖入了正式上线状态。由于任何新的更改都需要你通知所有用户,你的开发速度已经慢得像爬行一样。

对 API 进行逆向工程并不像您想象的那么困难,除非您采取措施阻止它,否则您可以假设人们会这样做。

通过隐蔽性实现安全,其理念是认为隐藏的东西因此是安全的。正如隐藏在壁橱里的圣诞礼物并非如此,网络安全也并非如此。如果您想确保您的私有 API 保持私密,请确保除非用户拥有正确的权限,否则无法访问它们。最简单的方法是将权限系统与 API 密钥绑定。如果 API 密钥无权使用该路由,请尽早放弃并返回状态为 403 的错误消息。

让你的身份证无法被猜测

我在之前关于对象 ID 的文章中提到过这一点,但值得在这里重温一下。如果您正在设计一个返回对象及其关联 ID 的 API,请确保这些 ID 无法被猜测或以其他方式进行逆向工程。如果您的 ID 只是简单的连续序列,那么往好了说,您是在无意中泄露您可能不想让别人知道的业务信息,往坏了说,这可能会导致潜在的安全事故。

举例来说,如果我在您的网站上进行购买并获得确认订单 ID“10”,那么我可以做出两个假设:

  1. 你的生意远没有你声称的那么多
  2. 我可能能够获取之前 9 个订单(以及所有未来订单)的信息,但我知道这些订单的 ID,所以不应该获取这些信息。

对于第二个假设,我可以尝试通过以您不希望的方式滥用您的 API 来了解有关您的其他客户的更多信息:



// If the below route isn't behind a permission system, 
// I can guess the ID and get potentially private 
// information on your other customers
curl https://api.example.com/v1/orders/9 

// Response
{
    "id": "9",
    "object": "order",
    "name": "Lady Jessica",
    "email": "jessica@benegesserit.com",
    "address": "1 Palace Street, Caladan"
}


Enter fullscreen mode Exit fullscreen mode

相反,通过使用UUID等技术,让你的 ID 难以猜测。使用本质上由随机数字和字母组成的字符串作为 ID,意味着你无法根据你现有的 ID 猜测下一个 ID 是什么。

虽然在便利性方面有所欠缺(使用“订单 42”比使用“订单 123e4567-e89b-12d3-a456-426614174000”要容易得多),但安全性方面的优势可以弥补这一不足。不过,别忘了添加对象前缀,使其更易于阅读。以 order_3LKQhvGUcADgqoEM3bh6pslE 格式生成订单 ID,将使您以及使用您 API 构建的人员的工作更加轻松。

为人类设计 API

关于如何设计 API 有很多资源,我希望本文能给您一些思考的空间,并激励您更深入地探索这个难题。

在 Stripe,我们非常重视 API 设计。我们内部有一份设计模式文档,其中包含我上面提到的内容以及更多内容。它包含了优秀和糟糕设计的示例、值得注意的例外情况,甚至还有一份关于如何向现有资源添加枚举等内容的指南。我最喜欢的部分是“不建议”部分,其中突出显示了我们 API 中目前存在的一些可疑设计示例,旨在警示未来的 Stripe 开发人员。

如果你喜欢这篇文章,可以看看“为人类设计 API”系列的其他文章。我还建议你加入“你不会讨厌的 API”社区,了解更多关于 API 设计的想法。

关于作者

Paul Asjes 的头像

Paul Asjes是 Stripe 的开发倡导者,负责撰写代码、与开发者沟通。工作之余,他喜欢酿造啤酒、制作肉干,以及和儿子玩马里奥赛车。

文章来源:https://dev.to/stripe/common-design-patterns-at-stripe-1hb4
PREV
如何在 2023 年成为一名出色的 Web 开发人员(前端和后端技巧)
NEXT
帮助我晋升为数据科学家的三本机器学习书籍