Factory menyelesaikan “objek dari mana diciptakan”; builder menyelesaikan “bagaimana objek dengan banyak parameter opsional dirakit dengan aman”. Konfigurasi sebuah server web bisa punya dua parameter wajib — host dan port — plus lima parameter opsional: TLS, batas koneksi, timeout, kompresi gzip, ukuran payload. Menyediakan constructor untuk setiap kombinasi menghasilkan deretan constructor yang saling memanggil dan sulit dibaca; menyediakan setter menghasilkan objek yang setengah jadi beredar di antara kode. Builder pattern menawarkan jalan ketiga: rakit langkah demi langkah lewat method berantai, lalu lahirkan objek utuh yang tervalidasi di satu panggilan build().
Masalah Constructor yang Memanjang #
Pendekatan naif menghasilkan apa yang sering disebut telescoping constructor: constructor dua parameter, tiga parameter, empat parameter, terus bertambah setiap ada opsi baru. Pemanggil constructor sembilan parameter harus mengisi false, 0, null untuk opsi yang tidak dipakai, dan dua boolean berdampingan adalah resep nilai tertukar tempat yang tidak terdeteksi compiler. Pendekatan kedua — constructor kosong plus setter — memang ringkas, tetapi objek hidup dalam keadaan setengah terisi: tidak ada satu momen pun yang bisa kita sebut “objek ini sudah valid”, sehingga validasi harus ditebar di banyak tempat dan setiap method harus curiga terhadap field yang belum diisi.
Builder dengan Method Berantai #
Builder memisahkan fase perakitan dari objek jadi. Fase perakitan dipegang class Builder yang sengaja boleh berubah-ubah; objek akhir dibuat sekali di akhir dan langsung dibekukan. Program berikut merakit konfigurasi server web dengan cara tersebut:
public class RakitServer {
public static void main(String[] args) {
// Konfigurasi lengkap: builder menyusun langkah demi langkah
ServerWeb produksi = new ServerWeb.Builder("api.example.com", 443)
.tls(true)
.batasKoneksi(200)
.timeoutBacaMillis(3_000)
.gzip(true)
.build();
System.out.println("produksi : " + produksi);
// Konfigurasi pengembangan: parameter opsional memakai nilai bawaan
ServerWeb pengembangan = new ServerWeb.Builder("localhost", 8080).build();
System.out.println("dev : " + pengembangan);
// Validasi terjadi di build(), bukan tersebar di banyak constructor
try {
new ServerWeb.Builder("api.example.com", 99_999).build();
} catch (IllegalArgumentException e) {
System.out.println("ditolak : " + e.getMessage());
}
}
}
class ServerWeb {
private final String host;
private final int port;
private final boolean tls;
private final int batasKoneksi;
private final int timeoutBacaMillis;
private final boolean gzip;
private ServerWeb(Builder b) {
this.host = b.host;
this.port = b.port;
this.tls = b.tls;
this.batasKoneksi = b.batasKoneksi;
this.timeoutBacaMillis = b.timeoutBacaMillis;
this.gzip = b.gzip;
}
@Override
public String toString() {
return host + ":" + port + " [tls=" + tls + ", koneksi=" + batasKoneksi
+ ", timeoutBaca=" + timeoutBacaMillis + "ms, gzip=" + gzip + "]";
}
// Builder: merakit objek banyak parameter dengan method berantai
static class Builder {
private final String host;
private final int port;
private boolean tls = false;
private int batasKoneksi = 50;
private int timeoutBacaMillis = 30_000;
private boolean gzip = false;
Builder(String host, int port) {
this.host = host;
this.port = port;
}
Builder tls(boolean aktif) {
this.tls = aktif;
return this;
}
Builder batasKoneksi(int jumlah) {
this.batasKoneksi = jumlah;
return this;
}
Builder timeoutBacaMillis(int millis) {
this.timeoutBacaMillis = millis;
return this;
}
Builder gzip(boolean aktif) {
this.gzip = aktif;
return this;
}
// Pusat validasi: objek lahir dalam keadaan valid atau tidak lahir sama sekali
ServerWeb build() {
if (port < 1 || port > 65_535) {
throw new IllegalArgumentException(
"port harus 1 sampai 65535, diterima: " + port);
}
if (batasKoneksi < 1) {
throw new IllegalArgumentException("batas koneksi minimal 1");
}
if (timeoutBacaMillis < 1) {
throw new IllegalArgumentException("timeout baca harus positif");
}
if (tls && port != 443) {
throw new IllegalArgumentException("tls aktif mengharuskan port 443");
}
return new ServerWeb(this);
}
}
}Output yang Dihasilkan:
produksi : api.example.com:443 [tls=true, koneksi=200, timeoutBaca=3000ms, gzip=true]
dev : localhost:8080 [tls=false, koneksi=50, timeoutBaca=30000ms, gzip=false]
ditolak : port harus 1 sampai 65535, diterima: 99999Mari kita bedah perannya satu per satu:
- Baris 49-56 (
static class Builder): builder adalah class dalam yang sengaja mutable — field opsional menampung nilai bawaan dan siap ditimpa. Parameter wajib (host,port) diikat langsung di constructor builder, sehingga pemanggil tidak mungkin lupa mengisinya. - Baris 58-79 (method
tls,batasKoneksi, dst.): setiap method menimpa satu field lalu mengembalikanthis. Nilai kembalian inilah yang memungkinkan rantai.tls(true).batasKoneksi(200)pada baris 4-8; gaya penulisan semacam ini disebut fluent interface. - Baris 81-97 (
build()): seluruh validasi berhimpun di satu method. Port di luar rentang, TLS yang bukan di port 443, batas korsi kosong — semuanya ditolak sebelum objek jadi; baris 16-20 memperlihatkan penolakanport 99_999dengan pesan yang jelas. - Baris 27-33 (
private ServerWeb(Builder b)): constructor objek jadi dibuatprivate, sehingga satu-satunya jalan penciptaan adalah lewat builder. Seluruh field objek jadifinal: setelahbuild()kembali, konfigurasi tidak bisa diubah siapa pun — aman dibagikan antar thread tanpa sinkronisasi tambahan.
Builder di JDK dan Ekosistem #
Pola ini bukan teori: StringBuilder merakit string potongan demi potongan sebelum toString() membekukannya; HttpRequest.newBuilder() di modul java.net.http merakit permintaan HTTP dengan method berantai; Locale.Builder melakukan hal serupa untuk locale. Di ekosistem pihak ketiga, Lombok menyediakan anotasi @Builder yang menghasilkan class builder secara otomatis, dan Spring Boot memakai builder untuk konfigurasi klien seperti WebClient — keduanya materi konfigurasi lanjutan di luar contoh inti chapter ini.
Satu catatan biaya: builder menambah satu class dan membuat penciptaan tiga langkah (bangun builder, isi, build()). Untuk objek dua-tiga parameter tanpa opsi, constructor biasa atau static factory tetap lebih tepat. Builder mulai membayar biayanya ketika parameter opsional melebihi tiga atau kombinasi parameter punya aturan silang seperti TLS dan port 443 tadi.
Objek kompleks sudah bisa dirakit dengan aman. Bab berikutnya berpindah dari bagaimana objek dibuat ke bagaimana perilaku dipilih: ketika aturan diskon bisa berganti saat program berjalan, strategy pattern mengambil alih.
Referensi #
- Martin Fowler: FluentInterface
- Oracle: JDK 25 API — HttpRequest.newBuilder
- Oracle: The Java Tutorials (dev.java)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
RakitServer.java
Unduh berkaspublic class RakitServer {
public static void main(String[] args) {
// Konfigurasi lengkap: builder menyusun langkah demi langkah
ServerWeb produksi = new ServerWeb.Builder("api.example.com", 443)
.tls(true)
.batasKoneksi(200)
.timeoutBacaMillis(3_000)
.gzip(true)
.build();
System.out.println("produksi : " + produksi);
// Konfigurasi pengembangan: parameter opsional memakai nilai bawaan
ServerWeb pengembangan = new ServerWeb.Builder("localhost", 8080).build();
System.out.println("dev : " + pengembangan);
// Validasi terjadi di build(), bukan tersebar di banyak constructor
try {
new ServerWeb.Builder("api.example.com", 99_999).build();
} catch (IllegalArgumentException e) {
System.out.println("ditolak : " + e.getMessage());
}
}
}
class ServerWeb {
private final String host;
private final int port;
private final boolean tls;
private final int batasKoneksi;
private final int timeoutBacaMillis;
private final boolean gzip;
private ServerWeb(Builder b) {
this.host = b.host;
this.port = b.port;
this.tls = b.tls;
this.batasKoneksi = b.batasKoneksi;
this.timeoutBacaMillis = b.timeoutBacaMillis;
this.gzip = b.gzip;
}
@Override
public String toString() {
return host + ":" + port + " [tls=" + tls + ", koneksi=" + batasKoneksi
+ ", timeoutBaca=" + timeoutBacaMillis + "ms, gzip=" + gzip + "]";
}
// Builder: merakit objek banyak parameter dengan method berantai
static class Builder {
private final String host;
private final int port;
private boolean tls = false;
private int batasKoneksi = 50;
private int timeoutBacaMillis = 30_000;
private boolean gzip = false;
Builder(String host, int port) {
this.host = host;
this.port = port;
}
Builder tls(boolean aktif) {
this.tls = aktif;
return this;
}
Builder batasKoneksi(int jumlah) {
this.batasKoneksi = jumlah;
return this;
}
Builder timeoutBacaMillis(int millis) {
this.timeoutBacaMillis = millis;
return this;
}
Builder gzip(boolean aktif) {
this.gzip = aktif;
return this;
}
// Pusat validasi: objek lahir dalam keadaan valid atau tidak lahir sama sekali
ServerWeb build() {
if (port < 1 || port > 65_535) {
throw new IllegalArgumentException(
"port harus 1 sampai 65535, diterima: " + port);
}
if (batasKoneksi < 1) {
throw new IllegalArgumentException("batas koneksi minimal 1");
}
if (timeoutBacaMillis < 1) {
throw new IllegalArgumentException("timeout baca harus positif");
}
if (tls && port != 443) {
throw new IllegalArgumentException("tls aktif mengharuskan port 443");
}
return new ServerWeb(this);
}
}
}