Fluent API:实践与理论

应用程序编程接口 (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 类StringBuilderStringBuilder,它实现了构建器设计模式并用于有效地构建复杂的字符串:

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 配方一:

1
2
3
4
class 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()XYbuild()

Fluent API recipe II:
用有限状态机描述 API 协议。

1
2
3
4
5
6
7
8
class 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 协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface 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 生成器。例如,SilverchainFlingTypeLevelLR都是流畅的 API 生成器,它们将表示 API 协议或 DSL 的上下文无关语法转换为流畅的 API。

结论

流畅的 API 由一系列连续的方法调用调用。制作优雅简洁的界面是一种流行的做法。在这篇文章中,我们学习了如何实现基本的 fluent API,展示了它们可以在编译时强制执行 API 契约,并且看到它们甚至可以用于嵌入特定领域的语言。

简历: Ori Roth 是 Technion 的博士生,研究类型论的元编程应用。Ori 是Fl​​ing—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();
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值