Lima prinsip SOLID kini lengkap kita kuasai, dan salah satu pola yang terus muncul di dalamnya adalah pembuatan objek: composition root merangkai dependency, kontrak dipenuhi implementasi baru, dan pemanggil tinggal menerima objek jadi. Factory Pattern merapikan pekerjaan tersebut — ia menyembunyikan detail pembuatan objek di satu tempat agar kode lain cukup meminta apa yang dibutuhkannya. Chapter ini membahas dua wujud utamanya di PHP — static factory method dan factory method — beserta tabel pembedanya, dan ditutup dengan diskusi jujur kapan factory justru tidak diperlukan.

Mengapa Kita Membutuhkan Factory #

Kata kunci new memang murah ditulis, tetapi pembuatan objek yang layak sering tidak sesederhana itu: koneksi database butuh DSN dan opsi error mode, nilai uang butuh normalisasi, dan objek domain butuh validasi sebelum dianggap sah. Ketika logika semacam itu ditulis ulang di banyak tempat, setiap perubahan aturan harus dikejar ke seluruh penjuru kode. Factory menyelesaikannya dengan satu gagasan sederhana: satu tempat yang bertanggung jawab penuh atas cara sebuah objek lahir, sisanya cukup memanggil tempat itu. Manfaat sekundernya segera terasa — titik pembuatan yang terpusat juga memudahkan pengujian dan perawatan, tema yang kita dalami di seluruh section ini.

Static Factory Method sebagai Constructor Bernama #

Bentuk factory paling ringan adalah static factory method: method static yang tugasnya mengembalikan instance dari classnya sendiri. Ia menjawab kelemahan mendasar constructor di PHP: constructor hanya boleh ada satu dan namanya selalu __construct, sehingga maksud pembuatan objek sering tidak terbaca. Perhatikan value object uang berikut:

php
<?php

final class NominalRupiah
{
    private function __construct(private readonly int $sen)
    {
    }

    public static function dariRupiah(int $rupiah): self
    {
        return new self($rupiah * 100);
    }

    public static function dariString(string $teks): self
    {
        $angka = (int) preg_replace('/[^0-9]/', '', $teks);

        return new self($angka * 100);
    }

    public function keRupiah(): int
    {
        return intdiv($this->sen, 100);
    }
}

$a = NominalRupiah::dariRupiah(50000);
$b = NominalRupiah::dariString("Rp50.000");

echo $a->keRupiah(), "\n";
echo $b->keRupiah(), "\n";
var_dump($a == $b);

Output yang Dihasilkan:

text
50000
50000
bool(true)

Mari kita bedah desainnya:

  1. Baris 5: constructor dibuat private. Tidak ada kode di luar class yang bisa menulis new NominalRupiah(...) — semua pembuatan harus melewati gerbang yang kita kendalikan.
  2. Baris 9-12: dariRupiah() adalah named constructor pertama. Ia menerima angka rupiah utuh dan menyimpannya dalam satuan sen, representasi internal yang bebas dari masalah pecahan desimal.
  3. Baris 14-19: dariString() adalah gerbang kedua dengan aturan berbeda: menebang semua karakter non-digit dari teks seperti "Rp50.000" dengan preg_replace(), lalu mengubahnya menjadi sen. Dua constructor dengan maksud berbeda — hal yang mustahil dilakukan __construct sendirian.
  4. Baris 27-28: pemanggil kini menulis maksud pembuatannya: dariRupiah atau dariString. Kedua objek dibuat lewat jalur berbeda, dan var_dump($a == $b) di baris 32 membuktikan keduanya berakhir pada nilai yang identik.

Selain nama yang ekspresif, static factory memberi kebebasan lain yang tidak dimiliki constructor: ia bisa mengembalikan instance yang sudah ada (caching), mengembalikan subtype, bahkan memvalidasi dan menolak input — semuanya tanpa mengubah signature __construct.

Menyembunyikan Konstruksi yang Rumit #

Kekuatan yang sama bekerja pada objek dengan persiapan panjang. Membangun koneksi PDO yang benar memerlukan DSN yang benar, kredensial, dan atribut mode error — detail yang tidak relevan bagi 95% kode aplikasi:

php
<?php

final class KoneksiAplikasi
{
    private function __construct(private PDO $pdo)
    {
    }

    public static function sqlite(string $berkas): self
    {
        return new self(new PDO("sqlite:" . $berkas));
    }

    public static function mysql(string $host, string $namaDb, string $user, string $password): self
    {
        $dsn = "mysql:host=$host;dbname=$namaDb;charset=utf8mb4";

        return new self(new PDO($dsn, $user, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]));
    }

    public function eksekusi(string $sql): void
    {
        $this->pdo->exec($sql);
    }

    public function jumlahTabel(): int
    {
        return (int) $this->pdo->query(
            "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'"
        )->fetchColumn();
    }
}

$koneksi = KoneksiAplikasi::sqlite(":memory:");
$koneksi->eksekusi("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, pembeli TEXT)");

echo "Jumlah tabel: ", $koneksi->jumlahTabel(), "\n";

Output yang Dihasilkan:

text
Jumlah tabel: 1

Perhatikan pembagian kerjanya: sqlite() di baris 9-12 merangkai DSN SQLite dari satu parameter, sedangkan mysql() di baris 14-22 menuntun penyusunan DSN MySQL plus dua atribut PDO dalam sekali panggil. Pemanggil di baris 37 cukup menulis KoneksiAplikasi::sqlite(":memory:") — tanpa pernah tahu bentuk DSN, tanpa perlu mengingat atribut wajib, dan tanpa bisa lupa mengaturnya. Ketika suatu hari tim memutuskan semua koneksi harus menyalakan mode exception, perubahan terjadi di satu method, bukan di setiap titik pembuatan koneksi.

Factory untuk Keluarga Objek #

Static factory juga layak berdiri sebagai class tersendiri ketika tugasnya memilih satu dari sekumpulan implementasi — sebuah keluarga objek yang terikat kontrak yang sama. Ini kombinasi factory + interface yang kita kenal dari chapter Open/Closed Principle :

php
<?php

interface MetodePembayaran
{
    public function bayar(int $nominal): string;
}

final class TransferBank implements MetodePembayaran
{
    public function bayar(int $nominal): string
    {
        return "Transfer bank Rp" . number_format($nominal, 0, ",", ".");
    }
}

final class DompetDigital implements MetodePembayaran
{
    public function bayar(int $nominal): string
    {
        return "Dompet digital Rp" . number_format($nominal, 0, ",", ".");
    }
}

final class KartuKredit implements MetodePembayaran
{
    public function bayar(int $nominal): string
    {
        return "Kartu kredit Rp" . number_format($nominal, 0, ",", ".");
    }
}

class FactoryPembayaran
{
    public static function buat(string $kode): MetodePembayaran
    {
        return match ($kode) {
            "transfer" => new TransferBank(),
            "dompet" => new DompetDigital(),
            "kartu" => new KartuKredit(),
            default => throw new InvalidArgumentException("Metode tidak dikenal: $kode"),
        };
    }
}

foreach (["transfer", "dompet", "kartu"] as $kode) {
    $metode = FactoryPembayaran::buat($kode);
    echo $metode->bayar(250000), "\n";
}

Output yang Dihasilkan:

text
Transfer bank Rp250.000
Dompet digital Rp250.000
Kartu kredit Rp250.000

Tiga hal membuat pola ini sehat:

  1. Baris 3-6: keluarga objek diikat kontrak MetodePembayaran — pemanggil tidak perlu tahu class konkret apa yang diterimanya.
  2. Baris 34-42: FactoryPembayaran::buat() memetakan kode string ke class konkret memakai match. Cabang default melempar InvalidArgumentException sehingga kode tak dikenal gagal cepat dan informatif, bukan diam-diam memilih salah satu metode.
  3. Baris 44-48: pemanggil bekerja sepenuhnya lewat abstraksi — $metode->bayar(250000) berlaku sama untuk semua anggota keluarga, dan metode baru cukup ditambahkan sebagai class plus satu baris pemetaan.

Simple Factory vs Factory Method #

Contoh FactoryPembayaran di atas adalah simple factory: satu class dengan satu method yang memutuskan sendiri semua pemetaan. Gang of Four mendefinisikan pola Factory Method yang lebih ketat: class creator hanya mendeklarasikan method pembuatan (biasanya abstrak), dan subclass-lah yang memutuskan produk apa yang dibuat:

php
<?php

interface PengirimNotifikasi
{
    public function kirim(string $pesan): void;
}

final class PengirimEmail implements PengirimNotifikasi
{
    public function kirim(string $pesan): void
    {
        echo "[email] $pesan\n";
    }
}

final class PengirimSms implements PengirimNotifikasi
{
    public function kirim(string $pesan): void
    {
        echo "[sms] $pesan\n";
    }
}

abstract class LayananNotifikasi
{
    abstract protected function buatPengirim(): PengirimNotifikasi;

    public function ingatkanPelanggan(string $nama): void
    {
        $pengirim = $this->buatPengirim();
        $pengirim->kirim("Halo $nama, tagihan Anda jatuh tempo besok.");
    }
}

final class LayananEmail extends LayananNotifikasi
{
    protected function buatPengirim(): PengirimNotifikasi
    {
        return new PengirimEmail();
    }
}

final class LayananSms extends LayananNotifikasi
{
    protected function buatPengirim(): PengirimNotifikasi
    {
        return new PengirimSms();
    }
}

(new LayananEmail())->ingatkanPelanggan("Budi");
(new LayananSms())->ingatkanPelanggan("Siti");

Output yang Dihasilkan:

text
[email] Halo Budi, tagihan Anda jatuh tempo besok.
[sms] Halo Siti, tagihan Anda jatuh tempo besok.

Inti polanya ada di baris 23-32: LayananNotifikasi adalah creator yang menulis alur kerja utuh — ingatkanPelanggan() mengambil pengirim lewat $this->buatPengirim() lalu memakainya — tetapi tidak pernah menyebut nama produk konkret. Setiap subclass (baris 34-48) mengisi keputusan itu lewat override buatPengirim(). Menambah saluran WhatsApp berarti menulis PengirimWhatsapp plus LayananWhatsapp tanpa menyentuh satu baris pun di creator — creator benar-benar tertutup untuk modifikasi. Perbedaan keduanya bisa dirangkum begini:

AspekSimple FactoryFactory Method
Pembuat keputusansatu method dengan match/switch di dalamnyasubclass creator, lewat override method abstrak
Menambah tipe baruedit isi factory (perubahan kecil, terkonsentrasi)tulis creator + produk baru, kode lama tak tersentuh
Alur kerjahanya memilih produkboleh memuat alur kerja lengkap di creator
Cocok untuk3-5 tipe, pemetaan sederhanakeluarga besar, pembuatan berbeda per varian
Biayasangat murahlebih banyak class

Kapan Factory Justru Tidak Perlu #

Factory membayar cicilan berupa class dan lompatan tambahan, jadi ia layak dibangun hanya ketika pembayarannya nyata: constructor bernama yang menghilangkan ambiguitas, konstruksi panjang yang perlu disembunyikan, atau keluarga objek yang benar-benar bertumbuh. Untuk class dengan satu implementasi stabil dan pembuatan satu baris, new DateTimeImmutable("2026-01-15") langsung di tempat kejadian justru paling jujur dan paling mudah dibaca — membungkusnya factory hanya menambah satu lompatan saat pembaca kode menelusuri asal objek.

Ringkasan #

Factory Pattern memusatkan pembuatan objek: static factory method menyediakan constructor bernama yang ekspresif dan mampu menyembunyikan konstruksi rumit, simple factory memilih satu anggota dari keluarga objek lewat kontrak bersama, dan factory method menyerahkan keputusan pembuatan kepada subclass creator. Semua bentuknya bekerja paling baik justru karena bersanding dengan interface — pembeli mendapat abstraksi, detail kelahirannya tersembunyi. Pattern berikutnya, Strategy, melangkah lebih jauh: bukan hanya membuat objek, tetapi menukarnya di tengah jalan selama aplikasi berjalan.

Referensi #