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:

text
== 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 Rp375000

Bedah program di atas:

  1. Baris 36-42 (class PabrikLaporan): satu-satunya tempat yang menyebut nama LaporanTeks dan LaporanCsv. Menambah tipe JSON nanti hanya menyentuh enum, factory ini, dan satu class baru — kode klien di main tidak berubah sama sekali.
  2. Baris 12-16 (perulangan di main): kode klien berjalan untuk seluruh TipeLaporan.values() tanpa satu pun switch; begitulah tampak klien yang benar-benar bebas dari pemetaan.
  3. Baris 72-92 (LaporanCsv): constructor dibuat private dan digantikan dua static factory bernama jelas — dari dan satuBaris. Dua kebutuhan yang beda tidak lagi dibedakan lewat parameter boolean yang 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:

text
[PabrikTeks] teks: apel, jeruk
[PabrikCsv] csv: apel;jeruk

Bedah singkat kedua program:

  1. Baris 19-30 (abstract class PabrikLaporan): buatLaporan() adalah factory method-nya; tampilkan() adalah template method yang memakai hasil penciptaan tanpa mengenal class konkret. Perhatikan tampilkan diberi final agar alur kerangka tidak bisa diubah subclass.
  2. Baris 32-55 (PabrikTeks dan PabrikCsv): masing-masing subclass mengisi buatLaporan() — yang satu mengembalikan implementasi lambda teks, yang lain CSV. Menambah PabrikJson berarti menulis subclass baru tanpa menyentuh induk maupun klien.
  3. Baris 9-11 (main): larik bertipe PabrikLaporan memperlakukan 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

LaporanPabrik.java

Unduh berkas
java
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;
    }
}

MetodePabrik.java

Unduh berkas
java
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";
    }
}