从石器时代到田园牧歌:如何对 API 统一建模
摘要
API 作为系统间交互的契约,在软件工程领域有着非常关键的地位。随着互联网时代的兴起,各式各样的应用百花齐放,API 的形态也随着技术的演进有了非常大的变化。那么如何规范化地描述 API,使它适应技术与业务的飞速发展,就成为了一个极富挑战性的问题。
首先,本文将从 API 建模面临的问题入手,例如类型系统、架构风格,来使大家对该领域有一个初步的了解。在这个部分,我们可以看到 Swagger、APIBlueprint、APIDoc.js、GraphQL 等被广泛采用的方案,以及它们之间的对比。
随后,本文将介绍新时代所面临的新问题,并以两个云厂商的方案 Smithy 和 Darabonba 为例,来学习业界如何对 API 建模问题进行高阶的抽象,并引入通用的模式与领域特定语言来解决这一类问题。
最后,孤影不成席,API 需要工具生态的支持才能够让更多的人使用。对于这个广泛的话题,本文将撷取其中的一片侧影 - 代码生成技术,分别从模版,转换器和控制流三个角度来近距离接触它,使大家能够直观地感受到,高阶抽象对于 API 建模的意义。
混沌未开
在 Swagger 等方案兴起之前,许多组织倾向于采用一个集中式的 API 编辑平台,带有图形化编辑器和数据服务存储。该方案会带来一些问题,例如:
编辑难:图形化编辑器必然带来一定的操作成本,对于工程师来说,代码化编辑和自动化交付是最核心的追求
集成难:编辑平台必须提供足量和充分的信息操作接口,才能够使下游通过自动化集成的方式,与平台保持最终一致,否则必然存在割裂的情形
变更难:组织内部对于 API 定义的准入限制策略频繁变化,甚至于审批工作流本身的实现也在不停变动,导致不断有新的系统需要集成,那么编辑平台需要一直跟进组织当前的工作流系统,存在高昂的维护成本。
随着时代的发展,大家开始逐渐认同,API 的定义,应该与代码同质化,一同编辑、集成与变更,甚至一同交付,而代码化后,就可以复用如 Git 等版本控制工具的工作流基础设施。
星星之火
主流开源方案对比
在本节中我们将介绍一些主流方案的基本构成与对比,例如 Swagger、APIBlueprint、APIDoc.js、GraphQL。
Swagger 是一种使用 YAML 来设计和编写 API 的规范与工具链,创建于 2010 年,并在 2015 被 SmartBear 收购,更名为 OpenAPI。Swagger 是最流行的 API 设计工具之一,被多种第三方工具所支持,具有非常广泛的影响力。
API Blueprint 是一种使用 Markdown 语法来设计和编写 API 的工具。项目建立于 2014 年,使用 Markdown 语法的优势在于,拥有良好的可读性,使得不同职能的团队协作更加容易。
apiDoc.js 是一种通过扫描代码注释来生成 API 文档的工具。项目于 2013 年启动,其特点是在代码注释中编写文档,支持多种编程语言,通常作为开发辅助工具来使用。
GraphQL 是一种面向查询的领域特定语言,于 2015 年由 Facebook 公开发布。GraphQL 严格来说是一种查询语言,与其它大多数 API 规范风格迥异。但由于它有着当前主流的 API 建模方法中,最为完备的类型系统,所以具备很高的研究价值。
这四种方案的特点足够鲜明,分别代表了这一类基础 API 建模方案的四种不同编写风格:配置、文档、注释和 DSL(领域特定语言)。
为什么说他们是一类方案?这四种方案具备共同的特点:「在 API 基础描述能力之上,基于一种架构风格有针对性地进行了扩展」 。这里有两个关键词,「 API 基础描述能力 」 和 「 架构风格」 ,本文将围绕这两个关键词开始我们的旅程。
API 基础描述能力
无论是哪一种方案,都有一些公共的性质,我们把这些公共的性质总结称为 API 的「 基础描述能力」。
API 的基础描述能力主要包括基础信息和类型系统。
基础信息,即请求、响应、错误的结构以及附加其上的描述信息。
类型系统,包括字段的类型信息,以及根据类型而附加的结构信息与约束规则。
基本构成这里不再赘述,主要讲一讲类型系统。
对于一个 API 来说,类型系统是必不可少的,类型系统是 API 类型安全的保障,也是 API 契约效力的重要组成部分。
如果缺少了类型系统,那么客户端将无法确定什么样的请求是合理的,从而难以达成服务端的期望。所以,类型系统的定义必然应该包含在 API 统一建模的能力当中。
在上图中,从复合类型以下的类型,都只能作为其他类型的一个容器,而不能够单独存在。例如,定义 array 类型时,必须额外定义数组中元素的类型,这种类型之间的级联关系,我们称之为类型的结构。
最后则是约束规则,除了类型本身语义所隐含的约束规则外,根据类型的不同,仍可以添加额外的约束规则,如长度、取值范围等。
这里有几个特殊的类型需要单独介绍:
enum
,枚举类型容纳一个简单类型,并且定义了该简单类型的所有可枚举值union
,联合类型容纳多个类型,并使该值满足多个类型中任意一个类型的约束null/not-null
,与一些编程语言不同,API 的空类型需要容纳一个其它类型,并使该值可选/非空,对于客户端校验和代码静态分析有特殊的意义
enum
、union
和 null
在不同方案中的实现方式有非常大的区别,例如对于 swagger 来说,enum
和 null
类型的信息被标注在字段的 enum
和 required
属性上,而 GraphQL 则定义了类型修饰符来描述 not-null
类型。
这些方案在类型系统上的支持情况如下:
架构风格
对于绝大多数开源解决方案来说,架构风格是在 API 设计之初就需要设定好的,例如 API 是 Restful 还是 RPC,协议里是否有特殊的信息需要附着在 API 的定义中?诸如此类。
其中 Swagger、APIBlueprint 和 APIDoc.js 都是以 RESTFul 作为主要的架构风格,RESTFul 是 Roy Thomas Fielding 博士在他 2000 年发表的博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出的,其在 API 语义描述方面的核心思想就是,「 使用一组预设的动作来触发资源的状态转化」 ,所以 RESTFul 是以资源为中心来组织的。
而 RPC(远程过程调用,Remote Procedure Call)则是另外一种常见的风格,客户端通过特定的协议,去调用远程服务的一个过程动作,返回结果。所以 RPC 风格的 API 是以行为为中心来组织的。
而 GraphQL 则提出了一种新的架构风格,而本文不涉及这种架构风格,这里就不再赘述,感兴趣的小伙伴可以移步参考文献进一步了解。
那么架构风格是否能够在事后变更呢,答案是可以的,我们在下一小节提到的「高阶抽象」中具体提及。
可以燎原
高阶抽象
上一章提到了一些主流的开源解决方案,但在实际工程场景下,这些方案的实践仍然会遇到一些问题,包括:
大多数方案都与某种特定的架构风格绑定。如果在一个组织内部,存在多种架构风格,那么必然会分裂为多种不同的技术方案。
大多数方案都缺少对于下游制品信息的抽象。API 的契约分为生产者与消费者,契约的生产者即编写者,而消费者实际上不仅仅只有 API 的调用方。形形色色的 API 制品,如 SDK、CLI、以及 API 相关的 SaaS 制品,也需要作为契约的消费者,获取 API 的信息。
要解决这两类问题,则需要对 API 的模型进行更高阶的抽象。
开源的方案中,由 AWS 开源的 Smithy 和 阿里云开源的 Darabonba,可以作为两个典型的案例,使我们从另一个角度,来观察业界如何进行这一类高阶的抽象。
形状与特质
如果在一个组织内部,多种架构风格并存,那么我们如何对架构风格进行统一建模?我们来看 AWS Smithy 项目对 API 进行的抽象:
首先要观察,架构风格之间都有哪些差异。对于 RESTFul 和 RPC 来说,差异主要在于 API 的组织方式,和协议、制品渠道所附加的一些额外的信息。
RESTFul 是围绕 Resource,而 RPC 是围绕行为来组织 API 的,那么如果把这些结构信息再进行一次高阶的抽象,那么我们就完成了第一个小目标。
AWS 将 API 的这些结构信息抽象为形状(Shape),除了类型系统的基本结构信息外,还包括服务(Service)、资源(Resource)和操作(Operation)等等。
而一些额外的信息,则作为特质(Traits)附着在这些形状(Shape)上,最终构成了完整的 API 定义,大家可以参考该链接的示例来感受一下。
这样的高阶抽象,使得 API 的结构(也包括架构风格)和具体的信息是充分正交的,互相解耦,防止语义上的冲突,同时为后面的 API 制品抽象打下基础。
模式
当我们把 API 的信息正交分解后,我们如何向 API 中添加下游所需的信息呢?比如 SDK、Cloud Formation、Terraform,以至于一些 SaaS 工具产品所需要的信息。
在上一小节中,我们看到 API 的信息在正交分解后,实际上成为了树形结构,对于树形结构的处理,我们自然想到了观察者模式(Visitor)。
Visitor 模式可以递归地去处理一个树上的节点,我们可以针对每一个特定用途的信息,去解析、甚至从外部系统中获取信息,并附着在树的节点上。
这是一个多趟(multi-pass)的过程,即针对每一种信息,实现一个独立的 Visitor,每次遍历注入一类信息。
Smithy 即采用这种方式,将 Cloud Formation 等 Traits 信息注入到了这棵树上。
领域特定语言
阅读上文的示例时,有细心的读者可能发现,Smithy 使用了一种特殊的语法,这是由 Smithy 定义的领域特定语言(DSL,Domain Specified Language)。
Darabonba 项目中,也同样采用了专有的领域特定语言来编写 API。
API 建模所使用的领域特定语言(DSL),跟我们常见的通用编程语言(GPL)有本质的区别,DSL 通常是该领域语义模型(Sementical Model)的一个抽象的外观(Facade),本质上还是对上文提到的抽象语义模型的描述。
为什么要采用领域特定语言来编写呢?笔者有着自己的猜测:
DSL 的编写成本更低,描述能力更强,可以不用关注数据的细节,写起来更优雅。
与代码生成技术有关,DSL 所能够注入的一些信息,是其它的方案所无法获取的,例如控制流信息。
这里值得一提的是,Darabonba 中除了包含 API 的语义模型信息外,也能够描述一些控制流信息,这使得该方案在 API 制品的代码生成上,有了更充分的想象空间。关于代码生成技术,我们将在下一节详细解读。
田园牧歌
上文中提到,API 的契约分为生产者与消费者。契约的生产者即编写者,而除调用方外,制品(包括 SDK、CLI、API 生态 SaaS 产品等)也需要作为契约的消费者,获取 API 的信息。那么为了保证 API 与制品的一致性,大多数的制品都需要采用代码生成技术来进行自动生成。
根据 Martin Fowler 在《Domain-Specific Languages》中的介绍,代码生成的方案包括:基于转换器的生成和基于模版的生成。API 制品生成虽然与 DSL 代码的生成有所区别,但思路却是一脉相承。
Martin Fowler 说过,DSL 是领域语义模型的一种外观,也就是说,无论 API 定义语言的写法是什么样,最终都可以将语法元素剥离,沉淀成为领域语义模型。
既然 AST 可以转换为可读性更高的语义模型,那么直接使用语义模型来进行代码生成是否可行呢?
答案是,要分场景。
模版
在前面的小节中,我们定义了 API 的语义模型,也就是我们在本文第一、第二节中提到的「 基本结构」 与「 高阶抽象」 所描述的内容。
在此基础上,我们可以将这个树状结构的模型数据作为上下文,通过模版渲染的方式,生成 API 的各种制品代码。这种方式比较直观,任何一种常见的模版引擎都可以达到目的,这里就不再赘述了。
模版渲染方案的优势在于:
有良好的可读性:通过编写和阅读模版,可以直观地感知到目标代码的模样
易于编写:相对于基于转换器的生成技术,模版生成技术较为简单,所需的领域知识更少
这两个优势对于团队内外的职责分离具有重要的价值,模版方案非常适合 API 建模工具开发与制品开发不归属于同一个团队的情况。
模版方案也有它的缺陷,模版引擎通常也是由一门 DSL 语言来描述的,使得该方案的的描述能力受到了模版引擎的局限。
转换器
基于转换器的生成,顾名思义,就是通过实现对抽象语法树(AST,Abstract Syntax Tree)的转换逻辑(或树翻译方案)来进行代码生成。
以 Darabonba 项目为例,Darabonba Python SDK Generator 有这样一段示例代码:
前文提到过,Visitor 模式是处理树形结构的利器。对于 AST 来说,如果将 AST 上每一个有关于 API 的节点(例如 API、字段等),打印出对应的目标代码,即可实现基于转换器的代码生成,更详细的实现可以阅读参考文献中提及的相关代码。
转换器生成方案的优势在于:
可以突破模版引擎描述能力的局限。因为转换器采用 GPL(通用编程语言)来实现
可复用性更强,因为通用编程语言可以进行一些工程上的抽象
展望:控制流
在基于转换器方案的基础上,笔者发现,Darabonba 在 DSL 中添加了一定的控制流信息,并将其应用在了 SDK 示例代码生成上。这是一个非常好的思路,添加控制流信息可以将 API 的编排逻辑也作为语义模型的一部分进行交付。
这不禁令人遐想,如果能够把每一个工具制品想象成一张由 API 与控制流信息构成的网状结构,每一个 API 调用逻辑都视为一张控制流子图,那么所有的 API 制品将构成一张巨大的网络,将可以做到许多令人激动的事情,例如:
所有的 API 工具制品将使用同一门 DSL 来编写和生成
静态分析工具可以面向所有制品对 API 进行静态分析,例如覆盖率、可达性和圈复杂度等等
不过实际的工程实践当中很难有这么理想化的情形,且将它付诸将来。
结语
API 统一建模领域的发展历程,仿佛是一场神奇的旅程,本文通过观察和对比 6 种主流的 API 建模方案,介绍了一些相对比较小众的领域知识,包括 API 的基础描述能力,架构风格,高阶抽象,与代码生成技术。
从混沌未开,到点点炊烟,最终走向工业与科技的变革。而我们前进的脚步,刚过半程,未来的路还很长,与君共勉。
参考文献
开源项目
图书
《Types and Programming Languages》,Benjamin C. Pierce,该书介绍了一些类型系统的理论基础,比较难读,如果你读完了,大佬请带我飞
《领域特定语言》,Martin Fowler 是著名的技术作者,大家可能读过他的《重构》、《企业应用架构模式》等书籍。这本书比较偏科普向,很适合没有接触过该领域,但工作中用到相关知识的同学开拓视野。
版权声明: 本文为 InfoQ 作者【李宇飞】的原创文章。
原文链接:【http://xie.infoq.cn/article/004c5f1a253edfd59f2afb35e】。文章转载请联系作者。
评论