写点什么

从石器时代到田园牧歌:如何对 API 统一建模

用户头像
李宇飞
关注
发布于: 2021 年 04 月 11 日
从石器时代到田园牧歌:如何对 API 统一建模

摘要

API 作为系统间交互的契约,在软件工程领域有着非常关键的地位。随着互联网时代的兴起,各式各样的应用百花齐放,API 的形态也随着技术的演进有了非常大的变化。那么如何规范化地描述 API,使它适应技术与业务的飞速发展,就成为了一个极富挑战性的问题。


首先,本文将从 API 建模面临的问题入手,例如类型系统、架构风格,来使大家对该领域有一个初步的了解。在这个部分,我们可以看到 Swagger、APIBlueprint、APIDoc.js、GraphQL 等被广泛采用的方案,以及它们之间的对比。


随后,本文将介绍新时代所面临的新问题,并以两个云厂商的方案 Smithy 和 Darabonba 为例,来学习业界如何对 API 建模问题进行高阶的抽象,并引入通用的模式与领域特定语言来解决这一类问题。


最后,孤影不成席,API 需要工具生态的支持才能够让更多的人使用。对于这个广泛的话题,本文将撷取其中的一片侧影 - 代码生成技术,分别从模版,转换器和控制流三个角度来近距离接触它,使大家能够直观地感受到,高阶抽象对于 API 建模的意义。


图:文中提及的 6 种 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 类型安全的保障,也是 API 契约效力的重要组成部分。


如果缺少了类型系统,那么客户端将无法确定什么样的请求是合理的,从而难以达成服务端的期望。所以,类型系统的定义必然应该包含在 API 统一建模的能力当中。


表 1:类型系统


在上图中,从复合类型以下的类型,都只能作为其他类型的一个容器,而不能够单独存在。例如,定义 array 类型时,必须额外定义数组中元素的类型,这种类型之间的级联关系,我们称之为类型的结构


最后则是约束规则,除了类型本身语义所隐含的约束规则外,根据类型的不同,仍可以添加额外的约束规则,如长度、取值范围等。


这里有几个特殊的类型需要单独介绍:


  • enum,枚举类型容纳一个简单类型,并且定义了该简单类型的所有可枚举值

  • union,联合类型容纳多个类型,并使该值满足多个类型中任意一个类型的约束

  • null/not-null,与一些编程语言不同,API 的空类型需要容纳一个其它类型,并使该值可选/非空,对于客户端校验和代码静态分析有特殊的意义


enumunionnull 在不同方案中的实现方式有非常大的区别,例如对于 swagger 来说,enumnull 类型的信息被标注在字段的 enumrequired 属性上,而 GraphQL 则定义了类型修饰符来描述 not-null 类型。


这些方案在类型系统上的支持情况如下:

表 2:各方案类型系统对比

架构风格

对于绝大多数开源解决方案来说,架构风格是在 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 进行的抽象:


来源 - AWS Smithy 语义模型


首先要观察,架构风格之间都有哪些差异。对于 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,每次遍历注入一类信息。


图例:高阶抽象与 Visitor


Smithy 即采用这种方式,将 Cloud Formation 等 Traits 信息注入到了这棵树上。

领域特定语言

阅读上文的示例时,有细心的读者可能发现,Smithy 使用了一种特殊的语法,这是由 Smithy 定义的领域特定语言(DSL,Domain Specified Language)。


Darabonba 项目中,也同样采用了专有的领域特定语言来编写 API。


图例:Smithy vs Darabonba


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 有这样一段示例代码:


// generate AST data by parserconst ast = DSL.parse(main, path.join(modulePath, 'main.dara'));// initialize generatorconst generator = new Generator(config, 'python');
generator.visit(ast);
复制代码


前文提到过,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 是著名的技术作者,大家可能读过他的《重构》、《企业应用架构模式》等书籍。这本书比较偏科普向,很适合没有接触过该领域,但工作中用到相关知识的同学开拓视野。

发布于: 2021 年 04 月 11 日阅读数: 192
用户头像

李宇飞

关注

坐地平观三尺剑,登楼远眺万点星。 2016.08.23 加入

专注于云计算,DevOps,基础架构等领域,目前在 UCloud 从事工具链研发工作。

评论

发布
暂无评论
从石器时代到田园牧歌:如何对 API 统一建模