Sepanjang section ini kita mengumpulkan peralatan yang mengesankan: lima prinsip SOLID, factory, strategy, adapter, decorator, repository, hingga Unit of Work. Perjalanan itu menumbuhkan kepercayaan diri — dan kepercayaan diri inilah yang justru perlu diawasi. Kebanyakan kode yang sulit dirawat bukan karena terlalu sedikit pattern, melainkan karena terlalu banyak: abstraksi yang dibangun untuk masa depan yang tidak pernah datang, interface yang hanya punya satu implementasi selamanya, dan layer yang menambah lompatan tanpa menambah kejelasan. Chapter penutup ini bukan tentang pattern baru, melainkan tentang keputusan kapan tidak memakainya — keterampilan yang membedakan pengguna pattern dari kolektor pattern.

YAGNI: Kode untuk Kebutuhan Hari Ini #

YAGNI — You Aren’t Gonna Need It — adalah prinsip dari Extreme Programming yang menyebut hal yang jarang kita ingin dengar: kode untuk kebutuhan yang dugaan akan datang hampir selalu menjadi beban, karena dugaan kita tentang masa depan biasanya salah. Kebutuhan nyata tiba dengan informasi yang tidak kita miliki hari ini — bentuk datanya, urutan prioritasnya, kendala operasionalnya. Membangun sebelum informasi itu hadir berarti menebak, dan tebakan yang keliru harus dibongkar nanti justru saat kebutuhan asli mulai mendesak.

Prinsip ini adalah sepupu dari nasihat klasik Kent Beck tentang urutan kerja: buat dulu bekerja (make it work), lalu buat benar (make it right), dan baru buat cepat (make it fast). Urutannya sengaja dibalik dari kebiasaan kita — kebenaran struktural dikejar setelah perilaku berjalan, bukan sebelumnya. Kalau diterapkan pada materi section ini, YAGNI berbunyi sederhana: tulis kode paling lurus yang memenuhi kebutuhan hari ini, biarkan SOLID dan pattern menunggu bukti bahwa masalahnya nyata. Pattern yang kita pelajari adalah obat, dan obat diminum saat sakitnya muncul — bukan sebagai vitamin pencegahan.

Speculative Generality dan Harganya #

Martin Fowler memberi nama pada code smell ini di katalog refactoring-nya: speculative generality — kerumitan yang ditambahkan “siapa tahu suatu saat butuh”. Bentuk paling jujurnya adalah membandingkan dua program yang menghasilkan keluaran persis sama. Yang pertama menyelesaikan tugasnya dalam beberapa baris:

php
<?php

$keranjang = [
    'beras 5 kg' => 68000,
    'gula 1 kg' => 17000,
    'kopi sachet' => 12500,
];

$total = array_sum($keranjang);

echo "Total belanja: Rp", number_format($total, 0, ',', '.'), "\n";

Output yang Dihasilkan:

text
Total belanja: Rp97.500

Yang kedua menyelesaikan tugas yang sama — dengan arsitektur. Ada interface untuk operasi kalkulasi, interface untuk kebijakan penyaringan, dan class MesinAgregasi yang merangkai keduanya:

php
<?php

interface OperasiKalkulasi
{
    public function terapkan(mixed $nilaiAwal, mixed $elemen): mixed;
}

final class PenjumlahanHarga implements OperasiKalkulasi
{
    public function terapkan(mixed $nilaiAwal, mixed $elemen): mixed
    {
        return $nilaiAwal + $elemen;
    }
}

interface KebijakanPenyaringan
{
    public function lolos(string $kunci, mixed $nilai): bool;
}

final class TerimaSemua implements KebijakanPenyaringan
{
    public function lolos(string $kunci, mixed $nilai): bool
    {
        return true;
    }
}

final class MesinAgregasi
{
    public function __construct(
        private OperasiKalkulasi $operasi,
        private KebijakanPenyaringan $penyaringan,
    ) {
    }

    public function proses(array $data, int $nilaiAwal = 0): int
    {
        $hasil = $nilaiAwal;

        foreach ($data as $kunci => $nilai) {
            if ($this->penyaringan->lolos($kunci, $nilai)) {
                $hasil = $this->operasi->terapkan($hasil, $nilai);
            }
        }

        return $hasil;
    }
}

$keranjang = [
    'beras 5 kg' => 68000,
    'gula 1 kg' => 17000,
    'kopi sachet' => 12500,
];

$mesin = new MesinAgregasi(new PenjumlahanHarga(), new TerimaSemua());
$total = $mesin->proses($keranjang);

echo "Total belanja: Rp", number_format($total, 0, ',', '.'), "\n";

Output yang Dihasilkan:

text
Total belanja: Rp97.500

Dua keluaran identik, satu perbedaan mencolok: versi kedua butuh sekitar enam kali lipat baris kode untuk sampai ke angka yang sama. Biayanya bukan hanya ukuran. Pembaca yang hanya ingin tahu “bagaimana total dihitung?” harus menelusuri tiga file mental: interface, implementasi TerimaSemua yang selalu mengembalikan true, lalu mesinnya. Pengujian makin kurang relevan — menguji PenjumlahanHarga terpisah sama sekali tidak menambah keyakinan dibanding menguji array_sum() bawaan PHP yang sudah teruji puluhan tahun. Dan ketika kebutuhan nyata akhirnya datang — misalnya “jumlahkan hanya produk bebas pajak” — kita tetap harus mengubah kode; kerumitan yang dibayar di muka tidak membeli apa pun.

Aturan Tiga: Abstrak Setelah Duplikasi Terlihat #

Kalau spekulasi adalah dosanya, apa obatnya? Bukan duplikasi buta, melainkan kesabaran menghitung. Rule of three — juga warisan Fowler — menyarankan menoleransi duplikasi sampai ia muncul untuk ketiga kalinya. Dua kejadian belum membentuk pola; baru pada kejadian ketiga bentuk yang benar-benar sama terlihat, dan saat itulah abstraksi yang kita ekstrak mendarat di tanah yang kokoh:

php
<?php

// Dua kebutuhan pertama: rumus pajak masih ditulis apa adanya
$hargaKopi = 54000;
echo "Kopi: Rp", number_format($hargaKopi * 110 / 100, 0, ',', '.'), "\n";

$hargaTeh = 32000;
echo "Teh: Rp", number_format($hargaTeh * 110 / 100, 0, ',', '.'), "\n";

// Kebutuhan ketiga datang: baru sekarang duplikasinya jelas dan bentuknya stabil
function hargaDenganPajak(float $harga): float
{
    return $harga * 110 / 100;
}

$hargaCokelat = 47500;
echo "Cokelat: Rp", number_format(hargaDenganPajak($hargaCokelat), 0, ',', '.'), "\n";
echo "Kopi (dihitung ulang lewat fungsi): Rp", number_format(hargaDenganPajak($hargaKopi), 0, ',', '.'), "\n";

Output yang Dihasilkan:

text
Kopi: Rp59.400
Teh: Rp35.200
Cokelat: Rp52.250
Kopi (dihitung ulang lewat fungsi): Rp59.400

Perhatikan betapa murahnya evolusi ini: dua baris pertama tidak menyesal pernah ditulis, karena bentuknya sederhana dan gampang dibaca; saat yang ketiga tiba, ekstraksi menjadi fungsi hargaDenganPajak() hanya menyentuh satu tempat dan langsung bisa dipakai mundur untuk menghitung ulang kopi. Prinsip yang beredar luas di komunitas — duplikasi jauh lebih murah daripada abstraksi yang salah — menemukan penjelasannya di sini. Duplikasi yang salah mudah diperbaiki: hapus satu, arahkan ke yang lain. Abstraksi yang salah menular ke semua pemanggilnya, dan perbaikannya berarti membongkar ulang seluruh jaringan panggilan yang sudah telanjur menempel.

Nyeri Abstraksi Prematur #

Kerusakan abstraksi prematur paling terasa bukan saat dibuat, melainkan saat kebutuhan ketiga tiba dengan bentuk yang tidak kita duga. Mari kita tonton prosesnya. Kebutuhan pertama: diskon persen. Kebutuhan kedua: potongan nominal dengan syarat harga minimal. Alih-alih dua fungsi kecil, kita memutuskan “sekalian” membuat satu fungsi serbaguna dengan mode dan flag:

php
<?php

function hitungDiskonSerbaguna(
    float $harga,
    string $mode,
    float $nilai,
    ?float $batasHarga = null,
    bool $pembulatanKeBawah = false,
): float {
    if ($batasHarga !== null && $harga < $batasHarga) {
        return $harga;
    }

    $hasil = $mode === 'persen'
        ? $harga * (100 - $nilai) / 100
        : $harga - $nilai;

    return $pembulatanKeBawah ? floor($hasil) : $hasil;
}

echo hitungDiskonSerbaguna(200000, 'persen', 20), "\n";
echo hitungDiskonSerbaguna(200000, 'nominal', 15000, batasHarga: 100000), "\n";
echo hitungDiskonSerbaguna(199999, 'persen', 15, pembulatanKeBawah: true), "\n";

Output yang Dihasilkan:

text
160000
185000
169999

Fungsinya bekerja, keluarannya benar — sampai di sini belum ada yang salah secara teknis. Sekarang kebutuhan ketiga datang, dan bentuknya di luar dugaan: promo beli dua gratis satu, yang tidak punya persen, tidak punya nominal, dan tidak peduli batas harga. Lihat apa yang terjadi pada masing-masing desain:

php
<?php

function hargaSetelahDiskonPersen(float $harga, float $persen): float
{
    return $harga * (100 - $persen) / 100;
}

function hargaSetelahPotonganNominal(float $harga, float $nominal): float
{
    return max($harga - $nominal, 0);
}

function totalBayarBeliDuaGratisSatu(float $hargaSatuan): float
{
    return $hargaSatuan * 2;
}

echo hargaSetelahDiskonPersen(200000, 20), "\n";
echo hargaSetelahPotonganNominal(200000, 15000), "\n";
echo totalBayarBeliDuaGratisSatu(25000), "\n";

Output yang Dihasilkan:

text
160000
185000
50000

Pada desain tiga-fungsi, kebutuhan baru masuk sebagai fungsi baru — satu tempat, tanpa menyentuh kode yang sudah teruji, persis semangat open/closed principle yang sudah kita pahami. Pada fungsi serbaguna, kebutuhan baru memaksa pilihan sulit: menambah parameter $jumlahUnitGratis? Mode baru 'beli2gratis1'? Setiap opsi menambah satu flag lagi pada signature yang sudah lima parameter, dan pemanggil lama kini harus membaca dokumentasi untuk memastikan flag baru tidak mengubah perilaku lamanya. Abstraksi yang terlanjur salah tidak bisa ditambahi — ia harus ditumbangkan, dan semua pemanggilnya ikut jatuh bersamanya.

Tanda-Tanda Kecanduan Pattern #

Overengineering jarang mengaku sebagai dirinya. Berikut daftar gejala yang paling sering muncul, lengkap dengan penawarnya:

GejalaPertanyaan penawar
Interface dengan satu implementasi, lama, tanpa rencana kedua“Pemanggil kedua dan implementasi kedua kapan tiba?”
Nama class seperti AbstractFactoryManagerProvider“Bisakah nama ini jujur satu baris?”
Setiap perubahan kecil menyentuh 4-5 file berlapis“Berapa lompatan untuk membaca satu alur?”
Wrapper yang isinya hanya meneruskan panggilan“Apa nilai tambah lapisan ini?”
Membahas pattern saat menjelaskan kode, bukan masalah“Masalah bisnis apa yang sedang diselesaikan?”
Folder src/ membengkak lebih cepat dari fitur yang jalan“Fitur mana yang baru selesai pekan ini?”

Ciri paling ringan untuk dideteksi adalah rasio antara jumlah lompatan dan jumlah kejelasan. Kode sehat bisa punya banyak file, tetapi setiap lompatan (class baru, interface baru, layer baru) harus dibayar kejelasan yang bisa dirasakan pembaca berikutnya. Kalau lompatan hanya memindahkan kode ke tempat lain tanpa menjelaskan apa pun, lompatan itu spekulasi.

Jalur Evolusi: Sederhana Dulu #

Menghindari overengineering bukan berarti menulis kode buruk dan menunggu ia membusuk. Jalurnya justru disiplin: mulai dari bentuk termurah, lalu naikkan levelnya hanya saat sinyal nyata muncul. Tabel berikut memetakan tangganya untuk skenario yang akrab — skrip kecil yang tumbuh menjadi aplikasi:

TahapBentuk kodeSinyal naik tahap
1Satu skrip PHP luruslogika mulai berulang di dalam satu berkas
2Fungsi-fungsi bernamafungsi yang sama dibutuhkan di berkas lain
3Class dengan method domainstate mulai dishare dan kehilangan bentuk
4Satu interface per titik ganti nyatabutuh implementasi kedua atau test tanpa layar eksternal
5Pattern penuh (repository, UoW, …)masalahnya persis masalah yang diselesaikan pattern itu

Dua hal membuat tangga ini bekerja. Pertama, setiap kenaikan tahap punya sinyal — bukti dari kode yang berjalan, bukan dari bayangan masa depan; factory pattern membayar cicilan justru karena pembuatannya menunggu konstruksi yang benar-benar rumit. Kedua, setiap tahap tetap merupakan kode yang rapi: fungsi bernama jelas, class kecil dengan satu tanggung jawab — kualitas dari single responsibility tidak pernah ditunda, yang ditunda hanyalah abstraksinya. Sebelum menambahkan abstraksi baru, tiga pertanyaan layak dijawab lebih dulu: siapa pemanggil keduanya, kapan kebutuhan itu tiba, dan apa bentuk nyatanya?

Kapan Pattern Terbayar #

Setelah seluruh section ini, adil kalau pattern dituntut membuktikan diri. Pattern terbayar ketika tiga hal terjadi bersamaan: masalahnya sudah pernah menyakiti (bukan dugaan akan menyakiti), solusinya sudah dicoba versi sederhananya dan ketemu batasnya, dan orang yang membaca kode mengenali masalah yang sama. Repository terbayar saat pengujian tanpa database menjadi kebutuhan nyata — bukan saat kita membayangkan mungkin butuh ganti database. Unit of Work terbayar saat operasi atomik lintas repository benar-benar bermunculan di banyak service. Strategy dan decorator terbayar saat varian keduanya, ketiganya, keempatnya sudah menunggu di antrian — momen ketika menambah cabang baru berubah dari pekerjaan lima menit menjadi ritual copy-paste berisiko.

Perkiraan yang beredar di industri — dan pengalaman sejarah perangkat lunak mendukungnya — adalah mayoritas kode berumur pendek: skrip laporan sekali jalan, prototype internal, alat migrasi. Untuk mereka, make it work adalah satu-satunya bab yang relevan, dan menuliskannya dengan jujur adalah bentuk profesionalisme tertinggi yang bisa diberikan. Sisanya yang memang bertahan lama akan memberi tahu — lewat test yang lambat, lewat duplikasi ketiga, lewat oncall yang berbunyi — dan saat itu terjadi, kita sudah punya seluruh peralatannya di tangan.

Ringkasan #

Menghindari overengineering adalah disiplin menunggu bukti: YAGNI menahan kita dari menulis kode untuk masa depan yang belum jelas bentuknya, speculative generality adalah tagihan untuk kerumitan yang ditebus di muka, dan aturan tiga memastikan abstraksi hanya diekstrak setelah polanya terlihat tiga kali. Tangga evolusi sederhana-dulu — skrip, fungsi, class, interface, pattern — memberi rute naik yang punya sinyal, bukan tebakan. Dengan bekal ini, perjalanan kita di dunia desain PHP lengkap: tahu kapan menggambar lapisan, dan tahu kapan menolaknya. Selanjutnya kita pindah ke medan yang benar-benar berbeda: membuka aplikasi PHP kita ke dunia luar lewat REST API Development .

Referensi #