Ketika sebuah tipe laporan baru ditambahkan, berapa banyak tempat di kode yang harus diubah? Bila jawabannya “setiap kode klien yang menulis new LaporanTeks(...) atau new LaporanCsv(...)”, penciptaan objek kita tersebar. Factory pattern memusatkan penciptaan tersebut di satu tempat sehingga kode klien cukup meminta berdasarkan maksud — “buatkan laporan CSV” — tanpa mengenal daftar class konkret. Chapter ini membahas tiga tingkatannya: simple factory untuk memusatkan switch, static factory sebagai named constructor ala List.of, dan factory method versi GoF yang menyerahkan keputusan penciptaan kepada subclass.
Masalah new yang Tersebar #
Operator new mengikat pemanggil ke class konkret. Ikatan ini punya dua biaya. Pertama, setiap penambahan tipe baru memaksa kita menyisir seluruh kode klien untuk menambah cabang if atau switch baru; satu tempat terlewat berarti laporan tipe baru tidak pernah tersedia di sana. Kedua, pengujian menjadi sulit karena kode klien tidak bisa menyisipkan implementasi palsu untuk keperluan uji. Keduanya adalah variasi dari masalah yang sama: keputusan “objek apa yang dibuat” tercampur dengan keputusan “objek itu dipakai untuk apa”.
Simple Factory #
Bentuk paling ringan adalah satu class atau method yang memegang seluruh pemetaan tipe ke class konkret. switch tidak dihapus dari muka bumi — pemetaan memang harus ada di suatu tempat — tetapi switch tersebut hidup di satu ruangan yang bisa dirawat sendiri, dan kode klien sepenuhnya bebas darinya:
import java.util.List;
public class LaporanPabrik {
public static void main(String[] args) {
List<Penjualan> data = List.of(
new Penjualan("Buku", 3, 85_000),
new Penjualan("Pena", 10, 12_000));
// Klien bebas switch: satu panggilan untuk setiap tipe laporan
for (TipeLaporan tipe : TipeLaporan.values()) {
Laporan laporan = PabrikLaporan.buat(tipe, data);
System.out.println("== " + tipe + " ==");
System.out.println(laporan.render());
}
// Static factory sebagai "named constructor": nama method menjelaskan maksud
Laporan ringkas = LaporanCsv.satuBaris(data);
System.out.println("== CSV satu baris ==");
System.out.println(ringkas.render());
}
}
enum TipeLaporan { TEKS, CSV }
interface Laporan {
String render();
}
record Penjualan(String produk, int jumlah, long hargaSatuan) {
long total() {
return (long) jumlah * hargaSatuan;
}
}
// Simple factory: satu-satunya tempat yang mengenal seluruh concrete class
class PabrikLaporan {
static Laporan buat(TipeLaporan tipe, List<Penjualan> data) {
return switch (tipe) {
case TEKS -> new LaporanTeks(data);
case CSV -> LaporanCsv.dari(data);
};
}
}
class LaporanTeks implements Laporan {
private final List<Penjualan> data;
LaporanTeks(List<Penjualan> data) {
this.data = data;
}
@Override
public String render() {
StringBuilder teks = new StringBuilder();
for (Penjualan p : data) {
teks.append(p.produk()).append(" x").append(p.jumlah())
.append(" = Rp").append(p.total()).append('\n');
}
teks.append("total: Rp").append(totalSemua());
return teks.toString();
}
private long totalSemua() {
long total = 0;
for (Penjualan p : data) {
total += p.total();
}
return total;
}
}
class LaporanCsv implements Laporan {
private final List<Penjualan> data;
private final boolean ringkasSatuBaris;
private LaporanCsv(List<Penjualan> data, boolean ringkasSatuBaris) {
this.data = data;
this.ringkasSatuBaris = ringkasSatuBaris;
}
// Named constructor untuk kebutuhan biasa
static LaporanCsv dari(List<Penjualan> data) {
return new LaporanCsv(data, false);
}
// Named constructor untuk kebutuhan ringkas
static LaporanCsv satuBaris(List<Penjualan> data) {
return new LaporanCsv(data, true);
}
@Override
public String render() {
if (ringkasSatuBaris) {
return data.size() + " baris data, total Rp" + totalSemua();
}
StringBuilder teks = new StringBuilder("produk,jumlah,total\n");
for (Penjualan p : data) {
teks.append(p.produk()).append(',').append(p.jumlah())
.append(',').append(p.total()).append('\n');
}
return teks.toString();
}
private long totalSemua() {
long total = 0;
for (Penjualan p : data) {
total += p.total();
}
return total;
}
}Output yang Dihasilkan:
== TEKS ==
Buku x3 = Rp255000
Pena x10 = Rp120000
total: Rp375000
== CSV ==
produk,jumlah,total
Buku,3,255000
Pena,10,120000
== CSV satu baris ==
2 baris data, total Rp375000Bedah program di atas:
- Baris 36-42 (
class PabrikLaporan): satu-satunya tempat yang menyebut namaLaporanTeksdanLaporanCsv. Menambah tipe JSON nanti hanya menyentuhenum, factory ini, dan satu class baru — kode klien dimaintidak berubah sama sekali. - Baris 12-16 (perulangan di
main): kode klien berjalan untuk seluruhTipeLaporan.values()tanpa satu punswitch; begitulah tampak klien yang benar-benar bebas dari pemetaan. - Baris 72-92 (
LaporanCsv): constructor dibuatprivatedan digantikan dua static factory bernama jelas —daridansatuBaris. Dua kebutuhan yang beda tidak lagi dibedakan lewat parameterbooleanyang mudah tertukar.
Static Factory sebagai Named Constructor #
JDK memakai gaya ini di mana-mana: List.of, Optional.of, Integer.valueOf, Map.of. Kekuatannya bukan sekadar pemusatan, melainkan nama. Constructor hanya dibedakan oleh daftar parameter — dua constructor (String, boolean) dan (String, int) sulit dibedakan di lokasi pemanggilan — sedangkan LaporanCsv.satuBaris(data) membaca seperti kalimat. Static factory juga bebas mengembalikan subtype atau instance yang di-cache, hal yang tak bisa dilakukan constructor.
Factory Method versi GoF #
Varian resmi GoF menggeser keputusan ke subclass: sebuah class mendeklarasikan method abstrak penciptaan, lalu setiap subclass mengisi objek apa yang dibuat. Class induk hanya bekerja dengan hasil method tersebut, sehingga kode kerangka tidak pernah mengenal class konkret:
import java.util.List;
public class MetodePabrik {
public static void main(String[] args) {
List<String> dataPenjualan = List.of("apel", "jeruk");
PabrikLaporan[] daftarPabrik = { new PabrikTeks(), new PabrikCsv() };
// Klien hanya mengenal PabrikLaporan; subclass yang memilih concrete class
for (PabrikLaporan pabrik : daftarPabrik) {
pabrik.tampilkan(dataPenjualan);
}
}
}
interface Laporan {
String render(List<String> data);
}
abstract class PabrikLaporan {
// Factory method (GoF): subclass yang menentukan objek apa yang diciptakan
abstract Laporan buatLaporan();
// Template method yang memakai hasil factory method
final void tampilkan(List<String> data) {
Laporan laporan = buatLaporan();
System.out.println("[" + namaPabrik() + "] " + laporan.render(data));
}
abstract String namaPabrik();
}
class PabrikTeks extends PabrikLaporan {
@Override
Laporan buatLaporan() {
return data -> "teks: " + String.join(", ", data);
}
@Override
String namaPabrik() {
return "PabrikTeks";
}
}
class PabrikCsv extends PabrikLaporan {
@Override
Laporan buatLaporan() {
return data -> "csv: " + String.join(";", data);
}
@Override
String namaPabrik() {
return "PabrikCsv";
}
}Output yang Dihasilkan:
[PabrikTeks] teks: apel, jeruk
[PabrikCsv] csv: apel;jerukBedah singkat kedua program:
- Baris 19-30 (
abstract class PabrikLaporan):buatLaporan()adalah factory method-nya;tampilkan()adalah template method yang memakai hasil penciptaan tanpa mengenal class konkret. Perhatikantampilkandiberifinalagar alur kerangka tidak bisa diubah subclass. - Baris 32-55 (
PabrikTeksdanPabrikCsv): masing-masing subclass mengisibuatLaporan()— yang satu mengembalikan implementasi lambda teks, yang lain CSV. MenambahPabrikJsonberarti menulis subclass baru tanpa menyentuh induk maupun klien. - Baris 9-11 (
main): larik bertipePabrikLaporanmemperlakukan semua pabrik seragam; perubahan pilihan penciptaan kini soal mengganti isi larik, bukan mengubah logika.
Penciptaan terpusat menyelesaikan masalah “objek dari mana”. Masalah berikutnya muncul saat satu objek butuh banyak parameter opsional: constructor mana yang harus dipanggil? Chapter berikutnya merakit jawabannya langkah demi langkah dengan builder pattern.
Referensi #
- Martin Fowler: Patterns of Enterprise Application Architecture — Catalog
- Oracle: JDK 25 API — List.of
- Oracle: The Java Tutorials (dev.java)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
LaporanPabrik.java
Unduh berkasimport java.util.List;
public class LaporanPabrik {
public static void main(String[] args) {
List<Penjualan> data = List.of(
new Penjualan("Buku", 3, 85_000),
new Penjualan("Pena", 10, 12_000));
// Klien bebas switch: satu panggilan untuk setiap tipe laporan
for (TipeLaporan tipe : TipeLaporan.values()) {
Laporan laporan = PabrikLaporan.buat(tipe, data);
System.out.println("== " + tipe + " ==");
System.out.println(laporan.render());
}
// Static factory sebagai "named constructor": nama method menjelaskan maksud
Laporan ringkas = LaporanCsv.satuBaris(data);
System.out.println("== CSV satu baris ==");
System.out.println(ringkas.render());
}
}
enum TipeLaporan { TEKS, CSV }
interface Laporan {
String render();
}
record Penjualan(String produk, int jumlah, long hargaSatuan) {
long total() {
return (long) jumlah * hargaSatuan;
}
}
// Simple factory: satu-satunya tempat yang mengenal seluruh concrete class
class PabrikLaporan {
static Laporan buat(TipeLaporan tipe, List<Penjualan> data) {
return switch (tipe) {
case TEKS -> new LaporanTeks(data);
case CSV -> LaporanCsv.dari(data);
};
}
}
class LaporanTeks implements Laporan {
private final List<Penjualan> data;
LaporanTeks(List<Penjualan> data) {
this.data = data;
}
@Override
public String render() {
StringBuilder teks = new StringBuilder();
for (Penjualan p : data) {
teks.append(p.produk()).append(" x").append(p.jumlah())
.append(" = Rp").append(p.total()).append('\n');
}
teks.append("total: Rp").append(totalSemua());
return teks.toString();
}
private long totalSemua() {
long total = 0;
for (Penjualan p : data) {
total += p.total();
}
return total;
}
}
class LaporanCsv implements Laporan {
private final List<Penjualan> data;
private final boolean ringkasSatuBaris;
private LaporanCsv(List<Penjualan> data, boolean ringkasSatuBaris) {
this.data = data;
this.ringkasSatuBaris = ringkasSatuBaris;
}
// Named constructor untuk kebutuhan biasa
static LaporanCsv dari(List<Penjualan> data) {
return new LaporanCsv(data, false);
}
// Named constructor untuk kebutuhan ringkas
static LaporanCsv satuBaris(List<Penjualan> data) {
return new LaporanCsv(data, true);
}
@Override
public String render() {
if (ringkasSatuBaris) {
return data.size() + " baris data, total Rp" + totalSemua();
}
StringBuilder teks = new StringBuilder("produk,jumlah,total\n");
for (Penjualan p : data) {
teks.append(p.produk()).append(',').append(p.jumlah())
.append(',').append(p.total()).append('\n');
}
return teks.toString();
}
private long totalSemua() {
long total = 0;
for (Penjualan p : data) {
total += p.total();
}
return total;
}
}
MetodePabrik.java
Unduh berkasimport java.util.List;
public class MetodePabrik {
public static void main(String[] args) {
List<String> dataPenjualan = List.of("apel", "jeruk");
PabrikLaporan[] daftarPabrik = { new PabrikTeks(), new PabrikCsv() };
// Klien hanya mengenal PabrikLaporan; subclass yang memilih concrete class
for (PabrikLaporan pabrik : daftarPabrik) {
pabrik.tampilkan(dataPenjualan);
}
}
}
interface Laporan {
String render(List<String> data);
}
abstract class PabrikLaporan {
// Factory method (GoF): subclass yang menentukan objek apa yang diciptakan
abstract Laporan buatLaporan();
// Template method yang memakai hasil factory method
final void tampilkan(List<String> data) {
Laporan laporan = buatLaporan();
System.out.println("[" + namaPabrik() + "] " + laporan.render(data));
}
abstract String namaPabrik();
}
class PabrikTeks extends PabrikLaporan {
@Override
Laporan buatLaporan() {
return data -> "teks: " + String.join(", ", data);
}
@Override
String namaPabrik() {
return "PabrikTeks";
}
}
class PabrikCsv extends PabrikLaporan {
@Override
Laporan buatLaporan() {
return data -> "csv: " + String.join(";", data);
}
@Override
String namaPabrik() {
return "PabrikCsv";
}
}