1. 概述
在这个教程中,我们将讨论流式接口设计模式,并将其与构建器模式进行比较。在探讨流式接口模式的过程中,我们会发现构建器只是其中的一种可能实现方式。接下来,我们将深入研究设计流式API的最佳实践,包括考虑因素如不可变性和接口分离原则。
2. 流式接口
流式接口是一种面向对象的API设计,它允许我们以可读且直观的方式链式调用方法。 实现它需要在同一个类中声明返回同类对象的方法。这样,我们就可以串联多个方法调用。这个模式常用于构建领域特定语言(DSL)。
例如,Java 8的Stream API
就使用了流式接口模式,它让用户能够以非常声明性的方式处理数据流。让我们看一个简单的例子,观察每次步骤后,一个新的Stream
是如何产生的:
Stream<Integer> numbers = Stream.of(1,3,4,5,6,7,8,9,10);
Stream<String> processedNumbers = numbers.distinct()
.filter(nr -> nr % 2 == 0)
.skip(1)
.limit(4)
.map(nr -> "#" + nr)
.peek(nr -> System.out.println(nr));
String result = processedNumbers.collect(Collectors.joining(", "));
我们可以注意到,首先通过静态方法Stream.of()
创建实现了流式接口的对象。然后,我们通过其公共API进行操作,可以看到每个方法都返回同一类。最后,我们通过返回不同类型的结束方法结束链。在示例中,这是一台收集器,返回一个String
。
3. 构建器设计模式
构建器设计模式是一种构造型设计模式,它将复杂对象的构建与其实现分离。构建器类实现了流式接口模式,允许逐步创建对象。
让我们来看一个构建器设计模式的简单用法:
User.Builder userBuilder = User.builder();
userBuilder = userBuilder
.firstName("John")
.lastName("Doe")
.email("[email protected]")
.username("jd_2000")
.id(1234L);
User user = userBuilder.build();
我们可以识别出上一个例子中讨论的所有步骤。流式接口设计模式由User.Builder
类实现,通过User.builder()
方法创建。接着,我们链式调用多个方法,指定User
的各种属性,每次调用都返回相同类型:User.Builder
。最后,通过build()
方法调用来退出流式接口,实例化并返回User
。因此,可以说构建器模式是流式API模式的唯一可能实现。
4. 不可变性
如果我们想创建一个流式接口的对象,就需要考虑不可变性。 上一节中的User.Builder
并不是一个不可变对象,它的内部状态会改变,总是返回同一个实例——即自身:
public static class Builder {
private String firstName;
private String lastName;
private String email;
private String username;
private Long id;
public Builder firstName(String firstName) {
this.firstName = firstName;
return this;
}
public Builder lastName(String lastName) {
this.lastName = lastName;
return this;
}
// other methods
public User build() {
return new User(firstName, lastName, email, username, id);
}
}
另一方面,每次返回一个新的实例也是可能的,只要它们具有相同的类型。让我们创建一个可用于生成HTML的流式类:
public class HtmlDocument {
private final String content;
public HtmlDocument() {
this("");
}
public HtmlDocument(String html) {
this.content = html;
}
public String html() {
return format("<html>%s</html>", content);
}
public HtmlDocument header(String header) {
return new HtmlDocument(format("%s <h1>%s</h1>", content, header));
}
public HtmlDocument paragraph(String paragraph) {
return new HtmlDocument(format("%s <p>%s</p>", content, paragraph));
}
public HtmlDocument horizontalLine() {
return new HtmlDocument(format("%s <hr>", content));
}
public HtmlDocument orderedList(String... items) {
String listItems = stream(items).map(el -> format("<li>%s</li>", el)).collect(joining());
return new HtmlDocument(format("%s <ol>%s</ol>", content, listItems));
}
}
在这种情况下,我们可以通过直接调用构造函数来获取流式类的实例。大多数方法返回一个HtmlDocument
,遵循模式。我们可以使用html()
方法结束链式调用,得到最终的String
:
HtmlDocument document = new HtmlDocument()
.header("Principles of O.O.P.")
.paragraph("OOP in Java.")
.horizontalLine()
.paragraph("The main pillars of OOP are:")
.orderedList("Encapsulation", "Inheritance", "Abstraction", "Polymorphism");
String html = document.html();
assertThat(html).isEqualToIgnoringWhitespace(
"<html>"
+ "<h1>Principles of O.O.P.</h1>"
+ "<p>OOP in Java.</p>"
+ "<hr>"
+ "<p>The main pillars of OOP are:</p>"
+ "<ol>"
+ "<li>Encapsulation</li>"
+ "<li>Inheritance</li>"
+ "<li>Abstraction</li>"
+ "<li>Polymorphism</li>"
+ "</ol>"
+ "</html>"
);
此外,由于HtmlDocument
是不可变的,链式调用的每个方法都会产生一个新的实例。换句话说,如果我们在文档末尾添加段落,它将成为不同的对象:
HtmlDocument document = new HtmlDocument()
.header("Principles of O.O.P.");
HtmlDocument updatedDocument = document
.paragraph("OOP in Java.");
assertThat(document).isNotEqualTo(updatedDocument);
5. 接口分离原则
接口分离原则,SOLID原则中的"I",教导我们避免大型接口。为了完全遵守此原则,我们的API客户端不应该依赖它从未使用的方法。
当我们构建流式接口时,要注意API的公共方法数量。我们可能会被诱惑添加越来越多的方法,导致接口变得庞大。例如,Stream API
有超过40个公共方法。让我们看看我们的流式HtmlDocument
的公共API如何发展。为了保留之前的示例,我们将在这个部分创建一个新的类:
public class LargeHtmlDocument {
private final String content;
// constructors
public String html() {
return format("<html>%s</html>", content);
}
public LargeHtmlDocument header(String header) { ... }
public LargeHtmlDocument headerTwo(String header) { ... }
public LargeHtmlDocument headerThree(String header) { ... }
public LargeHtmlDocument headerFour(String header) { ... }
public LargeHtmlDocument unorderedList(String... items) { ... }
public LargeHtmlDocument orderedList(String... items) { ... }
public LargeHtmlDocument div(Object content) { ... }
public LargeHtmlDocument span(Object content) { ... }
public LargeHtmlDocument paragraph(String paragraph) { .. }
public LargeHtmlDocument horizontalLine() { ...}
// other methods
}
有许多方法可以保持接口更小。其中之一是将方法分组,并使用小型、紧密关联的对象组合HtmlDocument
。例如,我们可以限制API为三个方法:head()
, body()
, 和 footer()
,并通过对象组合来创建文档。注意这些小型对象本身也暴露了流式API:
String html = new LargeHtmlDocument()
.head(new HtmlHeader(Type.PRIMARY, "title"))
.body(new HtmlDiv()
.append(new HtmlSpan()
.paragraph("learning OOP from John Doe")
.append(new HorizontalLine())
.paragraph("The pillars of OOP:")
)
.append(new HtmlList(ORDERED, "Encapsulation", "Inheritance", "Abstraction", "Polymorphism"))
)
.footer(new HtmlDiv()
.paragraph("trademark John Doe")
)
.html();
6. 总结
在这篇文章中,我们了解了流式API设计。我们探讨了构建器模式如何是流式接口模式的一种实现方式。然后,我们进一步深入流式API,并讨论了不可变性的问题。最后,我们解决了大型接口的问题,并学习了如何分割API以遵循接口分离原则。
本文使用的完整代码可以在GitHub上找到。