Java 中的 Fluent Interface 和 Builder Pattern 之間的區別
一、概述
在本教程中,我們將討論流暢的界面設計模式,並將其與構建器模式進行比較。當我們探索流暢的接口模式時,我們將意識到構建器只是一種可能的實現。從那裡,我們可以深入研究設計流暢 API 的最佳實踐,包括不變性和接口隔離原則等考慮因素。
2. 流暢的界面
Fluent Interface 是一種面向對象的 API 設計,它允許我們以可讀和直觀的方式將方法調用鏈接在一起。要實現它,我們需要聲明從同一類返回對象的方法。因此,我們將能夠將多個方法調用鏈接在一起。該模式通常用於構建 DSL(領域特定語言)。
例如,Java8 的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(", "));
正如我們所注意到的,首先我們需要創建實現流暢 API 模式的對象,在我們的例子中,這是通過靜態方法Stream.of()
實現的。在此之後,我們通過其公共 API 進行操作,我們可以注意到每個方法如何返回相同的類。我們以一個返回不同類型的方法結束,結束鏈。在我們的示例中,這是一個返回String
Collector
。
3. 建造者設計模式
構建器設計模式是一種創建型設計模式,它將復雜對象的構造與其表示分離。 Builder
類實現了流暢的接口模式,並允許逐步創建對象。
讓我們看一下構建器設計模式的直接用法:
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.
最後,我們通過實例化並返回User
build()
方法調用退出流暢的界面。因此,我們可以有把握地說,構建器模式是流暢 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 的流暢 API 的類:
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 OOP")
.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 OOP</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 OOP");
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();
六,結論
在本文中,我們了解了流暢的 API 設計。我們探討了構建器模式如何只是流暢接口模式的一種實現。然後,我們深入研究了流暢的 API 並討論了不變性問題。最後,我們解決了大接口的問題,並學習瞭如何拆分我們的 API 以遵守接口隔離原則。
可以在 GitHub 上找到本文中使用的完整代碼。