Java RecordBuilder 實用指南
1. 簡介
Java 16 中引入的Java 記錄提供了一種簡潔的方式來建模不可變資料。它們會自動產生建構函式、存取器、 equals()
、 hashCode()
和toString()
方法,從而減少了樣板程式碼並提高了可讀性。
儘管有這些好處,記錄也存在一些明顯的限制。例如,所有欄位都必須在記錄頭中聲明,不允許使用 setter 方法,並且由於其隱式final
屬性,擴展性受到限制。這些限制使得記錄在需要靈活建立物件時並不理想,尤其是在涉及可選參數或修改現有實例的場景中。
為了解決這些限制, RecordBuilder庫提供了一個簡單而有效的解決方案。它使用建構器模式增強了記錄功能,彌合了不變性的優雅與靈活構建的實用性之間的差距。
2. 入門
要開始使用RecordBuilder
,我們首先需要將註解處理器新增至建置設定。對於 Maven 用戶,如下所示:
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
<version>47</version>
</dependency>
設定完成後,我們將使用@RecordBuilder
註釋我們的記錄,並可選擇實現它產生的With
接口,以啟用內聯建構器和流暢的withX()
方法。
3.為什麼要使用RecordBuilder
?
乍一看,純形式的記錄功能強大——它們以最少的語法為我們提供了簡潔、不可變的資料結構:
public record Person(String name, int age) {}
然而,它們的全參數構造函數和缺乏設定器使得它們在我們需要靈活性時顯得僵化。在使用可選值或可變值來建構資料的領域,這很快就會成為限制。我們可以編寫自己的建構器來解決這個問題,但這樣做會重新引入那些記錄本應消除的樣板程式碼。
RecordBuilder
優雅地彌補了這一缺陷。只需一個註解,我們就能夠以流暢、安全且易讀的方式建構和修改記錄實例。它支援分階段建構器、 withX()
方法、自訂鉤子等,同時又不失不變性。例如,假設我們像這樣註解記錄:
@RecordBuilder
public record Person(String name, int age) implements PersonBuilder.With {}
由此, RecordBuilder
產生了一整套建構器方法、 withX()
存取器,甚至是靜態工廠助手,所有這些都遵循不變性的最佳實踐。
4. 使用生成的建構器
讓我們使用RecordBuilderDemo
類別來了解建構器改進我們的工作流程的一些實用方法。
首先,我們按照標準方式建構初始記錄:
Person p1 = new Person("foo", 123);
assertEquals("foo", p1.name());
assertEquals(123, p1.age());
接下來,我們透過產生的withName()
或withAge()
方法更新各個欄位:
Person p2 = p1.withName("bar");
assertEquals("bar", p2.name());
assertEquals(123, p2.age());
Person p3 = p2.withAge(456);
assertEquals("bar", p3.name());
assertEquals(456, p3.age());
在這裡,我們保留了不變性,每次轉換時只修改目標欄位。即便如此, RecordBuilder
還能提供更多功能。我們可以使用流暢的建構器語法,基於先前的實例建構一個全新的實例:
Person p4 = p3.with()
.age(101)
.name("baz")
.build();
assertEquals("baz", p4.name());
assertEquals(101, p4.age());
當多個欄位需要更改時,這種樣式可以提高清晰度。它避免了構造函數鍊式調用,並使每次轉換的意圖更加明確。正如我們接下來將看到的,該建構器還支援基於消費者的內聯更新,從而實現更具表現力的修改邏輯。
5.進階功能
RecordBuilder
的一大亮點是支援內聯、基於消費者的修改。以下是使用 lambda 修改記錄的方法:
Person p5 = p4.with(p -> p.age(200).name("whatever"));
assertEquals("whatever", p5.name());
assertEquals(200, p5.age());
此外,我們可以在建構器上下文中應用條件邏輯:
Person p6 = p5.with(p -> {
if (p.age() > 13) {
p.name("Teen " + p.name());
} else {
p.name("whatever");
}
});
assertEquals("Teen whatever", p6.name());
assertEquals(200, p6.age());
為了獲得更多控制,我們還可以使用靜態建構器工廠,這在記錄實例之外操作時特別有用:
Person p7 = PersonBuilder.from(p6)
.with(p -> p.age(300).name("Manual Copy"));
assertEquals("Manual Copy", p7.name());
assertEquals(300, p7.age());
或者,我們可以直接將更新應用於各個欄位:
Person p8 = PersonBuilder.from(p6)
.withName("boop");
assertEquals("boop", p8.name());
assertEquals(200, p8.age());
這種靜態形式在使用分離式建構器、外部資料轉換或測試工具時尤其有用。因此,它清晰地分離了建構邏輯和業務邏輯,使建構器模式成為跨服務層的多功能工具。
6.自訂和選項
RecordBuilder,
超越了基本的建構器生成,提供了自訂功能,使我們能夠根據特定領域的需求調整建構器。透過這種方式,我們支援分階段建構器,以便在編譯時強制執行必需字段,確保在構造之前始終設定必要的值。這有助於我們避免細微的運行時錯誤,並提高物件創建邏輯的安全性。
我們也可以把RecordBuilder
與測試框架或依賴注入工具集成,使其成為設定 Fixture 或註入部分建構物件的絕佳選擇。能夠將建構器邏輯與應用程式架構保持一致,同時又不失其不變性的優勢,使得RecordBuilder
既靈活又易於投入生產。
7. 比較: RecordBuilder
與 Manual Builder
我們完全可以為記錄編寫自己的建構器,尤其是在只有幾個欄位的情況下。**然而,隨著記錄的增長和演變,手動建構器很快就會成為維護難題。**每次更改記錄都需要更新整個建構器:新增欄位、建構函式邏輯、 withX()
方法以及build()
邏輯,所有這些都需要手動調整。
RecordBuilder
透過註解處理消除了這項負擔。它在編譯時產生建構器,並使其與記錄結構保持完美同步。對記錄頭的任何修改(新增、刪除或重新排序欄位)都會自動處理。這消除了一個常見的錯誤來源,並提高了長期可維護性。
此外, RecordBuilder
還引入了一些實用的模式,例如基於消費者的更新和靜態克隆方法,這些模式手動實現起來需要付出巨大的努力。就開發人員的效率、正確性和一致性而言, RecordBuilder
價值遠遠超出手動建構器。
8. 結論
RecordBuilder 函式庫讓 Java 記錄的使用更加輕鬆實用。它能夠產生流暢、不可變的建構器,無需手動建構函式或更新方法。
它的真正優勢在於平衡性:靈活的更新而不失不變性,以及無需繁瑣的模板即可實現的富有表現力的程式碼。無論我們建構的是 DTO、API 回應還是配置對象,它都能自然地融入我們的工作流程。幾乎無需任何設置, RecordBuilder
就能成為一款簡潔、可擴展的 Java 開發工具。
本文的完整原始碼可以在 GitHub 上找到。