API 设计
目录
一、API 设计基础
明确 API 设计目标是什么?
当人们在技术选择上存在分歧时,往往是因为他们在目标上不一致
如何确定你的目标?抓重点
你的工作中遇到的最关键的问题是什么?如果不能抓住
关键问题
,那就不算出色的工作。为了找到解决方案,你必须时刻盯着关键问题
API 设计的基础定义(交互过程)
-
分为客户端和服务端
-
客户端通过调用服务的的 action 获得数据
-
交互必须快速、简单、安全
整个互联网都是建立在 URI 的基础之上的
API 的复杂性
-
软件通常在遇到变更(change)时,难于修改
-
软件本身功能点多,功能复杂性
-
软件建立在一定的假设的前提上,这些前提很容易变更,包括:
-
从业务视角来看,我们的业务需求持续变更
-
存在老系统变更
-
业务需求变更
-
业务场景变更
-
-
从技术实现视角来看,我们总是追求更好的解决方案
-
选用的数据库变更
-
选用的实现技术
-
选用的程序语言变更
-
-
-
-
软件集成(integrate)复杂问题,集成通常是多维度的、海量的
-
有些系统,缺失基础的 API 造成集成复杂
-
每个系统有自己独立的 API ,为了集成,你需要全部学习这些 API,(n 的平方 - n) 造成集成复杂
-
关键 API
-
尽管很复杂,区分一些解决关键问题的:
关键 API
-
其他 API 只负责模块间的通信
主要的 API 设计风格(style)
-
实体 CRUD 接口 (Entity-oriented),又叫 Rest 风格接口
-
使用代码操纵另一个模块暴露的实体(通过操作实体,改变行为)
-
栗如:
{ type: "/log", ... } POST /logs GET /logs/1 DELETE /logs/1 ...
-
-
功能(behavier)接口(Procedure-oriented)
-
使用代码调用另一个模块的功能接口(通过行为,修改实体)
-
栗如:
// 通常会有很多接口配合使用 /getAllLogs/getAllSuccessLogs/verrifyd /verifyLogLocation/locationNeeded /locationNeeded/createLocation /createLocation/getPlatform ...
-
-
Rest 风格接口便于统一管理,避免每个系统有自己的 API 风格,降低学习成本
-
但不要和你的后端数据模型一致,每个 Entity 包含多个 Table 数据模型
-
只是使用统一的 API 定义风格
-
关键 API ,API 属性选取
-
全应用统一的 API 设计风格
-
统一的传输模型(分布式系统通信)
-
自由的扩展(free to implementation assumptions)
统一的 API 设计风格
-
RestAPI 的矛盾
-
restAPI 希望:摒弃 HTTP 之上的 web 层概念,用 HTTP 解决一切。
-
但是RestAPI 没有必需的,查询或版本(version)的处理
-
-
如何设计统一风格的 API
-
定义通用的,有实际意义的 URL,就和定义表名一样。栗如:/customers,/accounts,/orders
-
定义通用的,有实际意义的资源 schema
-
定义其他复杂接口时,不得扰乱这些基础的定义
-
-
示栗:
{ // 常见错误:该接口缺乏应用程序标识 `logs` id: "12345", type: "/log", ... } { // 为 ID 提供应用程序标识 id: "/logs/12345", type: "/log", ownerID: "98765" ... } { id: "/logs/12345", type: "/log", // 错误:如果 ownerID 不是 person 而是机构? ownerID: "98765" ... } { id: "/logs/12345", type: "/log", // 指定 identity 为一个 URI owner: "rpc的url/98765" ... } { id: "/logs/12345", type: "/log", // 不合适,identity 指定为,可能会修改的 name owner: "rpc的url/name" ... }
应用集成的最基本问题是:存储在不同的应用程序中的实体之间的转换
一个最主要的原因是:缺乏应用程序标识,数据 identity,
12345
依赖于log
应为log.12345
,ownerID
依赖于URI
-
如何设计 query API
https://domain.com/person/Joe/pets/Lassie
改进为:(query API 多次调用会得到不同的结果)
https://domain.com/search?type=/dog&name=Lassie&owner=(name=Joe)
-
当设计非 关键 API 时:重点考虑:效率高,易于解析,与编程语言匹配
-
当设计 关键 API 时:应考虑独立于现有技术和使用模式,考虑其适应变化能力
-
统一的 API 设计其他原则
-
不要传递包含不同的类型数据的集合,这样不便于解析。栗如:List<Object>
-
接口应该传递预先定义好的属性(closed content)
-
不要传递索引集,栗如:
// 使用 userId 做为索引组织数据,限定了返回格式,不能适用未来变化 open to change { user-id-a: { data: { id: "xxx", picture: "photo-url", createTime: "xxx", ... } }, user-id-b: { data: {...} } }
应改为平凡的(flat)
[ { id: "xxx", owner: "user-id-a", picture: "photo-url", createTime: "xxx", ... }, ]
-
不要传递动态类型,可为 String 可为 Object
-
避免值为 null/not-null
-
不要让数据存在歧义
-
不要使用 boolean 来代表 3 个值
-
-
使用简单格式的 JSON
-
简单格式 JSON 对象都必须对应于 API 数据模型中的一个实体
-
定义了一个名为 self 的特殊键值对,对应的数据模型实体
// 错误示栗 { "properties": { "name": "Martin", "bornOn": "1957-01-05" }, "links": [{ "rel": "bornIn", "href": "http://www.scotland.org#" }] }
-
-
版本
-
实体版本(新的版本,新的功能)
-
实体可以有多种格式,实体格式版本(同一个内容的不同属性别名)
-
历史版本(版本回退等)
-
-
不要在 url 中添加 版本号
{ id: "/xxx/12345", name: "Lassie", owner: "yyy/98765" }
不要使用
// 因为不是所有的实体都有 v2 版本,/v2/Lassie 将得不到值,服务器不好处理 { id: "/v1/xxx/12345", name: "Lassie", owner: "/v1/yyy/98765" }
-
每个实体必须有一个直接访问的无版本号(version-free)的 URI
-
允许客户端使用指定格式请求指定版本
-
选择性的提供指定版本的 URL
结论
-
如果相互耦合的模块,需要高效的交互,考虑 gRPC
-
如果需要为适用未来变化,做集成,直接使用 HTTP、JSON
二、API 设计基本原则
- 封装
- 信息隐蔽原则
- API 应隐藏内部细节
- 信息隐蔽原则
- 单一职责原则
- 考虑 API 的外部组织形式的内聚性
- 接口隔离原则 ISP
- 考虑方法的粒度(实体接口),功能范围(功能接口)
- 迪米特法则
- 减少业务耦合,提升内聚性
- 考察调用其他模块、被其他模块调用时的耦合
- 减少业务耦合,提升内聚性
- 开闭原则
- 尽量使用分布式 消息对象 输入输出
- 易用性原则
- 符合大众习惯,减少上手成本
- 其他
- 取个好的名字
- 非关键 API (一般为实体接口,见上文)统一风格
- 做好 API 版本管理,兼容
- 之类的 ………………