API 设计是微服务设计中非常重要的环节,代表服务之间交互的方式,会影响服务之间的集成。
通常来说,一个好的 API 设计需要满足两个主要目的:
一是平台独立性。任何客户端都能够消费 API,无需关注系统内部的具体实现。API 需采用标准的协议和消息格式为外部提供服务。传输协议和传输格式不应侵入到业务逻辑之中,即系统应随时具备支持不同传输协议和消息格式的能力。
二是系统可靠性。当 API 已经发布且非 API 版本发生改变时,API 应对契约负责,不能导致数据格式出现破坏性的修改。在 API 需要进行重大更新时,应采用版本升级的方式进行修改,并为旧版本预留一定的下线时间窗口。
在实践中发现,API 设计是一件颇具难度的事情,同时也很难衡量其设计是否优秀。从系统设计以及消费者的角度来看,以下是一些较为简单的设计原则。
1. 使用成熟度合适的 RESTful API
RESTful 风格的 API 具有一些天然的优势,例如通过 HTTP 协议降低了客户端的耦合,具有极好的开放性。因此越来越多的开发者使用 RESTful 这种风格设计 API,但是 RESTful 只能算是一个设计思想或理念,不是一个 API 规范,没有一些具体的约束条件。因此在设计 RESTful 风格的 API 的时候,需要参考 RESTful 成熟度模型。

2. 避免简单封装
API 应当对业务能力进行封装,避免单纯的封装使得 API 完全变成数据库操作接口。比如,标记订单状态为已支付,应提供类似 POST /orders/1/pay 这样的 API,而不是 PATCH /orders/1 后通过具体字段去更新订单。原因在于订单支付有着具体的业务逻辑,可能涉及众多复杂操作,若采用简单的更新操作,会将业务逻辑泄露到系统之外。同时,系统外也会知晓 “订单状态” 这个内部使用的字段。更为重要的是,这样做破坏了业务逻辑的封装,还会对其他非功能需求产生影响,例如权限控制、日志记录以及通知等。
3. 完全穷尽,彼此独立
API 之间应尽量遵循完全穷尽、彼此独立(MECE)原则,不应提供相互叠加的 API。这样做的好处在于不会出现重复的 API,从而避免在维护和理解上带来复杂性。那么,如何做到完全穷尽和彼此独立呢?一个简单的方法是使用表格来设计 API,标注出每个 URI 所具备的能力。

资源 URL 的设计若来源于 DDD(领域驱动设计)领域建模就会非常简单,以聚合根作为根 URL,实体作为二级 URI 进行设计。聚合根之间应完全没有任何联系,实体与聚合根之间的责任应当明确。产生这类问题的根源还是在于缺乏合理的抽象。如果在 API 中可以通过用户组来操作用户,又可以通过用户的 URI 来操作用户所属的用户组,这里面的问题就在于缺少了 “成员” 这一概念。用户组下面本质上并不是用户,而是用户和用户组的关系,即成员。
4. 版本化
一个对外开放的服务,有极大的概率会发生变化。业务的变化可能会导致 API 参数、响应数据结构以及资源之间的关系被修改。一般情况下,字段的增加不会影响旧的客户端运行。然而,当出现一些具有破坏性的修改时,就需要采用新的版本,将数据导向到新的资源地址。版本信息的传输可以通过 URI 前缀、Header 或者 Query 这几种方式来实现。
5. 合理命名
在设计 API 的时候,命名会涉及多个地方,包括 URI、请求参数以及响应数据等。通常来说,最为主要且最难做到的一点是实现全局命名的统一。其次,在命名时需要注意以下几点:尽可能与领域名词保持一致,比如聚合根、实体、事件等;在 RESTful 设计的 URI 中使用名词复数;尽可能不要过度简写;尽可能使用不需要编码的字符。用领域名词来对 API 进行设计命名并非一件特别困难的事情。识别出的领域名词可以直接作为 URI 来使用,如果存在多个单词的连接,可以使用中横线。
6. 安全
安全是任何软件设计都必须加以考虑的事项。对于 API 设计而言,暴露给内部系统的 API 与开放给外部系统的 API 略有不同。对于内部系统,更多的是考虑其是否足够健壮,要对接收的数据进行充分验证并给出错误信息,而不是不加辨别地接收任何信息,以免内部业务逻辑因边界值问题而变得难以理解。
而对于外部系统的 API 则面临更多挑战,比如错误的调用方式、接口滥用以及浏览器消费 API 时因安全漏洞导致的非法访问。因此,在设计 API 时应该考虑相应的应对措施。针对错误的调用方式,API 不应进入业务处理流程,而应及时给出错误信息;对于接口滥用的情况,需要制定一些限速方案;对于一些浏览器消费者的问题,可以让 API 返回一些安全增强头部。

