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上找到。