应用程序编程接口 (API)使从代码中访问应用程序成为可能。在这种情况下,应用程序可以是单个类、软件库或模块、外部工具或服务(如MongoDB),甚至是物理工件(如Google Assistant)。API 的用户是其他程序员,他们喜欢支持错误管理的简单直观的界面,以备不时之需。
那么,我们如何编写更好的 API 呢?此外,当然,适当的文档,API 的可用性很大程度上受其设计的影响。在这篇文章中,我们讨论了流畅的 API,它们是由一种时髦且流行的设计技术产生的。我们了解什么是 fluent API,它如何使程序员受益,以及如何编写富有表现力的 fluent API。我们还展示了流式 API 对特定领域语言的适用性。
Fluent API 通常与面向对象的编程语言相关联。尽管我们主要展示 Java 代码摘录,但等效的 fluent API 可以用任何 OO 编程语言组成,例如 C#、Go、Scala 等。
Fluent API 简介
考虑 Java 类StringBuilder。StringBuilder
,它实现了构建器设计模式并用于有效地构建复杂的字符串:
1
2
3
4
5
6
|
StringBuilder builder = new StringBuilder();
builder.append( "foo" );
builder.append( "bar" );
builder.replace( 4 , 6 , "az" );
builder.reverse();
String result = builder.toString(); // "zaboof"
|
该类StringBuilder
有一个流畅的 API,这意味着它的方法可以被连续调用,如下所示:
1
2
3
4
5
6
|
String result = new StringBuilder()
.append( "foo" )
.append( "bar" )
.replace( 4 , 6 , "az" )
.reverse()
.toString(); // "zaboof"
|
这一连贯的 API 调用序列称为连贯的方法调用链,或简称为链。为了StringBuilder
流畅,它的方法被设置为返回当前对象:
1
2
3
4
|
public StringBuilder append(String str) {
...
return this ; // yields the current object
}
|
尽管方法append()
不产生任何东西,但它确实返回了this
对象,其唯一目的是允许流畅的调用。当方法append()
返回 aStringBuilder
时,它的调用可以紧跟另一个StringBuilder
方法调用,依此类推。最后,方法toString()
通过返回一个字符串实例来结束链,实现StringBuilder
.
流畅的 API 通过允许将多个语句缩写为单个表达式来提高代码的可读性。例如,通过用StringBuilder
流畅的链替换上面的命令式方法调用,我们使代码更短,并且我们不再需要StringBuilder
变量。
广义上讲,要为类制作一个流畅的 API Fluent
,让其方法返回类型Fluent
;返回其他类型(如toString()
)的方法旨在密封流畅的链,并可选地产生它们的计算结果。但是流畅的方法实际上返回了哪些对象呢?与StringBuilder
, 和一般的构建器一样,让流畅的方法返回当前对象是最简单的方法。
Fluent API 配方一:
1234class
Fluent {
Fluent fluentMethod() { ...
return
this
; }
Result getResult() { ... }
}
在遵循这个秘诀的流式 API 中,命令式和流式调用样式是可以互换的。但是,如果 fluent 方法返回新的 API 实例,则情况可能并非如此:return new Fluent();
而不是return this;
. API 实例化可用于使流式链不可变,或者它可以简单地被实现所需要。为避免混淆,使用流畅 API 的正确方法通常在其文档中进行说明。
使用 Fluent API 执行协议
虽然流畅的 API 可以在动态编程语言(如 Python 和 JavaScript)中实现,但它们在静态语言(如 Java 和 C++)中尤其有效。一个原因可能是静态语言的语法往往更加严格和繁琐,因此流畅的链为程序员省去了更多麻烦。然而,静态语言中流式 API 真正令人着迷的特性是它们能够在编译时强制执行API 协议。
API 协议(或合同)是一组定义正确 API 使用的规则。为了演示,让我们设计一个流畅的 API 来构建电子邮件:
1
2
3
4
5
6
|
Mail mail = new MailBuilder()
.from( "donald.t@mail.com" )
.to( "joe.b@mail.com" )
.subject( "Congratulations on recent promotion" )
.body( "Yours truly, Donald" )
.build();
|
该MailBuilder
协议建立了撰写合法邮件的指导方针:
⇒一封邮件包括一个“发件人”字段、一个或多个“收件人”、可选的“主题”和一个文本“正文”,按照这个确切的顺序。
在上面讨论的流式 APIStringBuilder
中,任何方法调用的组合都可以编译,即使它破坏了协议。为了避免运行时错误,我们必须在类文档中说明协议细节(用户必须阅读并遵守它们)。
尽管如此,静态编程语言中的流畅 API 可以早在编译时检测到 API 滥用。例如,我们的邮件 API 禁止调用from()
后跟subject()
调用,因为它跳过了发件人字段 ( to()
)。在设计 API 时,我们可以保证返回类型from()
不包含方法subject()
;这样,包含 的链from().subject()
破坏了协议,甚至无法编译,因为subject()
调用无法静态解析。
为了使用流畅的 API 执行协议,我们首先将其形式化为(确定性)有限状态机(FSM) 图。下面的 FSM 精确地捕获了邮件 API 协议。
该图由节点组成,这些节点说明了可能的 API 状态,以及它们之间的标记有向边,描述了应用状态转换的 API 方法。例如,邮件 API 以Empty 邮件状态开始,但是当方法from()
被调用时,API 状态变为Sender set,表示已设置sender字段。我们还看到主题字段是可选的,因为可以通过调用直接从状态Receiver转换到状态Bodybody()
。一个完整的 API 调用以接受状态结束,用双线边框标记;在我们的例子中,我们只允许在设置主体build()
后调用。
用 FSM 表示协议后,我们可以直接在 fluent API 中实现它。我们把每一个状态变成一个类,把每一条边变成一个方法;给定一条从状态X到状态Y标记为f的边,我们将方法添加到类并为该方法提供返回类型。对于每个接受状态,我们还添加了终止方法 ( )。f()
X
Y
build()
Fluent API recipe II:
用有限状态机描述 API 协议。
12345678class
State {
State loop() { ... }
OtherState transition() { ... }
}
class
OtherState {
YetAnotherState otherTransition() { ... }
Result getResult() { ... }
}
例如,Receiver 集合状态编码如下:
1
2
3
4
5
|
class ReceiverSet {
ReceiverSet to(String receiver) { ... }
SubjectSet subject(String subject) { ... }
BodySet body(String content) { ... }
}
|
生成的 fluent API 本质上模拟了主机类型系统中的 FSM,因此只有尊重协议的 fluent 链才能编译:
1
2
3
4
5
6
|
Mail mail = new EmptyMail() // returns EmptyMail
.from( "joe.b@mail.com" ) // returns SenderSet
.to( "donald.t@mail.com" ) // returns ReceiverSet
.subject( "Re: Congratulations on recent promotion" ) // returns SubjectSet
.body( "Thank you." ) // returns BodySet
.build(); // returns Mail
|
例如,如果subject()
上面链中的调用被省略,导致非法邮件,那么代码将无法编译,因为对body()
from的调用ReceiverSet
无法解析。
除了强力执行协议外,流式 API 还可以与某些 IDE 服务巧妙地集成。自动完成是一种常见的 IDE 功能,它建议可能的代码延续。当应用于流畅的 API 时,自动完成通过显示哪些调用可以继续该链,同时忽略会破坏它的方法,主动引导程序员通过 API。
不幸的是,流畅的 API 实现最终可能会变得混乱且不可维护,因为它是由多个类组成的。如果两条 FSM 边具有相同的标签,那么我们必须将它们编码为两个不同 API 类中的相同方法。此外,由于流式方法可能不会返回它们所在的类,它们必须实例化一个新对象,并将链中收集的任何中间数据传递给它。这两个问题都可以通过将流畅的 API 状态编码为接口而不是类来解决。然后,我们创建一个实现所有接口的主类,因此它的方法可以简单地返回this
实例。这样,我们不仅将 API 逻辑集中在单个类中,而且还避免了单个链中的多个对象实例化。假设主类对用户不可见,这种替代设计仍然提供流畅 API 的好处。
Fluent API recipe III:
用有限状态机描述 API 协议。
1234567891011121314interface
InitialState {
InitialState foo();
OtherState bar();
}
interface
OtherState {
YetAnotherState bar();
Result baz();
}
private
class
APIImpl
implements
InitialState, OtherState, ... {
APIImpl foo() { ...
return
this
; }
APIImpl bar() { ...
return
this
; }
Result baz() { ... }
}
public
static
InitialState startChain() {
return
new
APIImpl(); }
Java中协议的最终编码MailBuilder
显示在帖子的末尾。
高级 Fluent API 技术
与 Java 和 C++ 等通用编程语言相比,领域特定语言(DSL) 是为特定任务或应用程序量身定制的。例如,SQL 是用于组合数据库查询的著名 DSL:
1
|
SELECT name FROM students WHERE grade>90
|
要从程序中提交查询,我们需要将SQL 作为 API 嵌入到我们的编程语言中。当然,可以用字符串编写 DSL 程序,然后在运行时解析它们:
1
|
var query = SQL.parse( "SELECT name FROM %s WHERE grade>90" , DB.students);
|
但是,在这种情况下,DSL 语法错误仅在运行时引发。其他更隐蔽的陷阱也是可能的。
流畅的 API 是嵌入 DSL 的巧妙解决方案。如果 DSL 的语法可以定义为 FSM,那么我们可以使用上面描述的方法将其转换为 fluent API,并在编译时捕获语法错误。我们将 DSL 关键字编码为方法,将 DSL 文字(如数字和字符串)和表达式编码为这些方法的参数:
1
2
|
var query = SQL.SELECT( "name" ).FROM(DB.students)
.WHERE(student -> student.grade> 90 );
|
Fluent APIs 近年来也受到了学术界的关注。虽然流畅的 API 设计艺术通常是民间传说,但研究人员正在寻找新方法来编码更复杂的协议和 DSL 系列。事实证明,通过使用泛型(多态)类型,可以将非常规协议(无法由 FSM 描述)编码为流畅的 API。然而,这些 API 通常是单一的,并且它们的结构是模糊的。因此,一个上升的研究趋势是提供一种从规范中自动生成流式 API 的工具——流式API 生成器。例如,Silverchain、Fling和TypeLevelLR都是流畅的 API 生成器,它们将表示 API 协议或 DSL 的上下文无关语法转换为流畅的 API。
结论
流畅的 API 由一系列连续的方法调用调用。制作优雅简洁的界面是一种流行的做法。在这篇文章中,我们学习了如何实现基本的 fluent API,展示了它们可以在编译时强制执行 API 契约,并且看到它们甚至可以用于嵌入特定领域的语言。
简历: Ori Roth 是 Technion 的博士生,研究类型论的元编程应用。Ori 是Fling—A Fluent API Generator的合著者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
import java.util.*;
public class Mail {
public String sender;
public List receivers = new ArrayList<>();
public String subject = "" ;
public String body = "" ;
private Mail() {} // Hide default constructor
public static EmptyMail builder() { return new MailBuilder(); }
interface EmptyMail {
SenderSet from(String sender);
}
interface SenderSet {
ReceiverSet to(String receiver);
}
interface ReceiverSet {
SubjectSet subject(String subject);
BodySet body(String body);
}
interface SubjectSet {
BodySet body(String content);
}
interface BodySet {
Mail build();
}
private static class MailBuilder implements EmptyMail,
SenderSet, ReceiverSet, SubjectSet, BodySet {
Mail mail = new Mail();
@Override public MailBuilder from(String sender) {
mail.sender = sender;
return this ;
}
@Override public MailBuilder to(String receiver) {
mail.receivers.add(receiver);
return this ;
}
@Override public MailBuilder subject(String subject) {
mail.subject = subject;
return this ;
}
@Override public MailBuilder body(String content) {
mail.body = content;
return this ;
}
@Override public Mail build() {
return mail;
}
}
// Usage example
public static void main(String[] args) {
Mail mail = Mail.builder()
.from( "donald.t@mail.com" )
.to( "joe.b@mail.com" )
.subject( "Congratulations on recent promotion" )
.body( "Yours truly, Donald" )
.build();
}
}
|