一般设计模式

优质
小牛编辑
130浏览
2023-12-01

空白响应

标准的 Delete 方法必须返回 google.protobuf.Empty 才能获得全局一致性。它还可以防止客户端重试时所依赖的元数据不可用。对于自定义方法,它们必须有自己的形如 XxxResponse 的消息,即使它们是空白的,因为很可能在方法功能变化后,需要返回额外的数据。

表示范围

用来表达范围的字段应当使用半开区间的命名约定,形如:[start_xxx,end_xxx),例如 [start_key,end_key][start_time,end_time)。 C++ STL 库和 Java 标准库通常使用这种半开区间语义。API 应避免使用其他表示范围的方法,例如 (index,count)[first,last]

资源标签

在面向资源的 API 中,资源模式由 API 来定义。为了让客户端将少量的简单元数据附加到资源(例如,将一个虚拟机标记为数据库服务器),API 应该使用 google.api.LabelDescriptor 中描述的资源标签设计模式。

为此,设计 API 时应该向资源定义中添加字段 map<string,string> 标签。

  1. message Book {
  2. string name = 1;
  3. map<string, string> labels = 2;
  4. }

长时运行的操作

如果一个 API 方法通常需要很长时间才能执行完成,那么它可以设计成向客户端返回一个长时间运行(Long Running Operation)资源,客户端可以使用该资源来跟踪进度并获取结果。该操作定义了一个标准接口,用于长时间运行的操作。各个 API 禁止为长时间运行的操作定义自己的接口,以避免不一致。

操作资源必须作为响应消息直接返回,并且操作的任何直接后果都应该反映在API中。例如,在创建资源时,尽管资源应该指示它没有准备好被使用,该资源也应该出现在LIST和GET方法中。操作完成后,如果方法不长时间运行,Operation.response字段应包含直接返回的消息。

列表分页

以列表形式获取的集合应该支持分页,即使结果集通常很小。

原理:即使向现有的 API 添加分页支持仅仅是从 API 表面视角添加,那也是一个会破坏 API 行为的更改。现有的客户端由于不知道分页,将错误地假设会接收到完整的列表结果,而实际客户端只接收到第一页。

为了使 List 方法支持分页(在页面中返回列表结果),API 应该

  • List 方法的请求消息中定义一个 string 类型字段 page_token。客户端使用此字段请求列表结果的特定页面。
  • List 方法的请求消息中定义一个 int32 类型字段 page_size。客户端使用此字段指定服务器一次返回的最大结果数。服务器仍然可以进一步限制在单个页面中返回的结果的最大数量,如果 page_size 为0,服务器将决定要返回的结果数。
  • List 方法的响应消息中定义一个 string 字段 next_page_token。此字段代表用于检索下一页的分页令牌。如果值为空字符串,则表示没有更多结果了。

要检索下一页结果,客户端应该在后续 List 方法调用(在请求消息的 page_token 字段中)传递前一次响应的 next_page_token 的值:

  1. rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);
  2. message ListBooksRequest {
  3. string name = 1;
  4. int32 page_size = 2;
  5. string page_token = 3;
  6. }
  7. message ListBooksResponse {
  8. repeated Book books = 1;
  9. string next_page_token = 2;
  10. }

当客户端传递除页面令牌之外的查询参数时,如果查询参数与页面令牌不一致,则服务必须使请求失败。

页面令牌的内容应该是一个 base64 编码过的PB(protocol buffer)。 这避免了兼容性问题,如果页面令牌包含潜在的敏感信息,那么该信息应该被加密。 同时服务必须防止通过以下方法篡改页面令牌以泄露数据:

  • 在后续请求时必须重新指定查询参数。
  • 在页面令牌中只引用服务器端的会话状态。
  • 页面令牌中加密并签名的的查询参数,需要在每次调用时都重新验证和重新授权。

分页的具体实现中也可以提供名为 total_sizeint32 字段来表示总数。

列出子集合

有时,API 需要让客户端在子集合之间执行List/Search方法。例如,图书馆 API 有一组书架,每个书架都有一组书籍,而客户想要在所有书架上搜索一本书。在这种情况下,建议对子集合使用标准List方法,并为父集合指定通配符集合标识“-”。对于 Library API 示例,我们可以使用以下 REST API 请求:

GET https://library.googleapis.com/v1/shelves/-/books?filter=xxx

注意:使用“-”而不是“*”的原因是为了避免需要进行URL转义。

从子集合获取唯一资源

有时,子集合中的一个资源具有在其父集合内唯一的标识符。在这种情况下,允许通过 Get 来检索它并且不需要知道其属于哪个父集合。同时,建议使用标准 Get,并为所有父集合指定通配符集合标识“-”。例如,在图书馆 API 中,如果图书在所有书架上的所有图书中是唯一的,则可以使用以下 REST API 请求:

GET https://library.googleapis.com/v1/shelves/-/books/{id}

上面这个请求的响应中的资源必须使用资源的规范名称,对于每个父集合,应使用实际父集合标识符而不是“-”。例如,上面的请求应该返回资源名为 shelves/shelf713/books/book8141,而不是 shelves/-/books/book8141

排序顺序

如果 API 方法允许客户端指定列表结果的排序顺序,请求消息应该包含一个字段:

string order_by = ...;

排序字符串值应该遵循 SQL 语法:逗号分隔字段列表。例如:“foo,bar”,默认排序顺序为升序,要为字段指定降序,应该在字段名后附加后缀 “desc”。例如:“foo desc,bar”

语法中的冗余空格字符无关紧要。“foo,bar desc”“ foo ,bar desc ” 是等效的。

请求验证

如果 API 方法有副作用,并且需要验证请求而避免此类副作用,请求消息应该包含一个字段:

bool validate_only = ...;

如果此字段设置为 true,则服务器禁止执行任何副作用逻辑,只能执行特定于实现的与完整请求一致的验证逻辑。

如果验证成功,则必须返回 google.rpc.Code.OK,任何使用相同请求消息的完整请求都不应返回 google.rpc.Code.INVALID_ARGUMENT。注意,请求可能由于其他错误(如 google.rpc.Code.ALREADY_EXISTS)或由于竞争而失败。

请求重复

对于网络 API,幂等 API 方法是更优的选择,因为它们可以在网络故障后安全地重试。然而,一些 API 方法无法容易地实现成幂等的。例如创建资源,并且需要避免不必要的重复。对于这种使用情况,请求消息应该包含唯一的 ID,例如 UUID,服务器将使用该唯一 ID 来检测复制,并确保请求仅被处理一次。

  1. // A unique request ID for server to detect duplicated requests.
  2. // This field **should** be named as `request_id`.
  3. string request_id = ...;

如果检测到重复请求,则服务器应该返回先前成功请求的响应,因为客户端很可能未收到先前的响应。

枚举默认值

每个枚举定义必须0 值条目开始,当未明确指定枚举值时,使用该条目。API 必须在文档中记录如何处理 0 值。

如果存在默认行为,则应该使用枚举值 0,并且 API 应在文档中记录预期行为。

如果没有默认行为,则枚举值 0 应该命名为 ENUM_TYPE_UNSPECIFIED,并且应该以错误 INVALID_ARGUMENT 来拒绝使用。

  1. enum Isolation {
  2. // Not specified.
  3. ISOLATION_UNSPECIFIED = 0;
  4. // Reads from a snapshot. Collisions occur if all reads and writes cannot be
  5. // logically serialized with concurrent transactions.
  6. SERIALIZABLE = 1;
  7. // Reads from a snapshot. Collisions occur if concurrent transactions write
  8. // to the same rows.
  9. SNAPSHOT = 2;
  10. ...
  11. }
  12. // When unspecified, the server will use an isolation level of SNAPSHOT or
  13. // better.
  14. Isolation level = 1;

可以0值使用一个惯用名称。例如,google.rpc.Code.OK 是指定缺少错误代码的惯用方法。 在这种情况下,OK 在语义上等同于枚举类型定义中的 UNSPECIFIED

在存在内在敏感和安全默认值的情况下,该值可以用于“0”值。 例如,BASIC资源视图枚举中的“0”值。

语法句法

在某些 API 设计中,有必要为某些数据格式定义简单的语法,例如可接受的文本输入。为了在 API 之间提供一致的开发体验并降低学习曲线,API 设计者必须使用 ISO 14977 扩展的巴斯克-诺尔格式(EBNF)句法来定义语法:

  1. Production = name "=" [ Expression ] ";" ;
  2. Expression = Alternative { "|" Alternative } ;
  3. Alternative = Term { Term } ;
  4. Term = name | TOKEN | Group | Option | Repetition ;
  5. Group = "(" Expression ")" ;
  6. Option = "[" Expression "]" ;
  7. Repetition = "{" Expression "}" ;

注意TOKEN 代表在语法之外定义的终止符。

整数类型

在 API 设计中,不应使用无符号整数类型,例如 uint32fixed32,因为一些重要的编程语言不能很好地支持它们,例如 Java 和 JavaScript,他们可能导致溢出错误。另一个问题是,不同的 API 非常喜欢在同一个场景中混用不匹配的有符号和无符号整数类型。

当有符号整数类型用于负值无意义的场景(例如尺寸或超时)时,值 -1(且只有 -1)可能用于指代特殊的含义,例如文件结尾(EOF)、无限超时、无限配额以及未知的年龄。这些用法必须清楚地记录在文档中,以避免混淆。API 创建者还应该记录隐式默认值 0 的行为。

部分响应

有时,一个 API 客户端只需要响应消息中的特定数据子集。为了支持这样的场景,一些 API 平台提供对部分响应的原生支持。Google API 平台通过响应消息中的字段掩码支持这一特性。对于任何 REST API 调用,都有一个隐式系统查询参数 $fields,它是 google.protobuf.FieldMask 值的 JSON 表示形式。响应消息将在发送回客户端之前由 $field 过滤。 API 平台为所有的 API 方法自动处理此逻辑。

GET https://library.googleapis.com/v1/shelves?$fields=name

资源视图

为了减少网络流量,有时允许客户端限制服务器在其响应中返回的资源的部分,返回资源的视图而不是完整的资源表示形式,这一做法是非常有用的。 API 中的资源视图通过向方法请求添加参数来实现该功能,该参数允许客户端指定需要在响应中接收的资源。

参数:

  • 应该enum 类型
  • 必须命名为 view

每个枚举值定义了资源的哪些部分(哪些字段)将在服务器的响应中返回。每个视图(view)值应该返回什么样的资源视图是由实现来定义的,同时应该在 API 文档中记录。

  1. package google.example.library.v1;
  2. service Library {
  3. rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
  4. option (google.api.http) = {
  5. get: "/v1/{name=shelves/*}/books"
  6. }
  7. };
  8. }
  9. enum BookView {
  10. // Server responses only include author, title, ISBN and unique book ID.
  11. // The default value.
  12. BASIC = 0;
  13. // Full representation of the book is returned in server responses,
  14. // including contents of the book.
  15. FULL = 1;
  16. }
  17. message ListBooksRequest {
  18. string name = 1;
  19. // Specifies which parts of the book resource should be returned
  20. // in the response.
  21. BookView view = 2;
  22. }

这个结构将映射成类似下面的 URL 地址:

  1. GET https://library.googleapis.com/v1/shelves/shelf1/books?view=BASIC

您可以在本设计指南的标准方法一章中找到有关定义方法、请求和响应的更多信息。

ETags

ETag 是一个非透明的标识符,允许客户端进行带条件的请求。为了支持 ETag,API 应该在资源定义中包含一个 string 字段 etag,其语义必须与ETag的通用用法相匹配。通常,etag包含由服务器计算出的资源的指纹。有关更多详细信息,请参阅 WikipediaRFC 7232 规范。

ETag 可以被强验证或弱验证,其中弱验证的 ETag 前缀为 W/。在此上下文中,强验证意味着具有相同 ETag 的两个资源具有逐字节完全相同的内容和相同的额外字段(例如,Content-Type)。这意味着经过强验证的 ETag 允许缓存部分响应并可以稍后用于组合。

相反,具有相同弱验证的 ETag 值的资源意味着在语义上等同,但不一定是逐字节相同的,因此不适合于缓存响应。

例如:

  1. // This is a strong ETag, including the quotes.
  2. "1a2f3e4d5b6c7c"
  3. // This is a weak ETag, including the prefix and quotes.
  4. W/"1a2b3c4d5ef"

输出字段

API 可能需要区分由客户端作为输入提供的字段,以及只由服务器返回的特定资源的输出字段。对于仅为输出的字段,字段属性应该在文档中记录。

注意,如果客户端在请求中对”仅输出字段“设值了,或者客户端指定了仅包含输出字段的 google.protobuf.FieldMask,则服务器必须接受请求,而不会出错。 这意味着服务器必须忽略那些”仅输出字段“。因为客户端通常会重用由服务器返回的资源,并将其作为另一个请求输入。例如,检索到的 Book 资源将在稍后用作 UPDATE 方法的请求。如果服务器验证“仅输出字段“,则客户端需要额外工作以清除这些“仅输出字段”。

  1. message Book {
  2. string name = 1;
  3. // Output only.
  4. Timestamp create_time = 2;
  5. }