Tujuh chapter terakhir membangun perangkat gaya fungsional lengkap — pure function, immutability, higher-order function, closure, pipeline, hingga composition. Pertanyaan yang wajar muncul di titik ini: haruskah kita membuang class dan berpindah total ke gaya fungsional? Jawaban jujurnya: tidak. PHP adalah bahasa multi-paradigma, dan mayoritas codebase PHP nyata — termasuk framework besar — mencampur kedua gaya dalam satu aplikasi. Chapter penutup ini membandingkan keduanya secara adil: kita selesaikan satu masalah yang sama dua kali, melihat tradeoff-nya dalam tabel, lalu menutup dengan panduan memilih yang pragmatis.

Masalah yang Sama, Dua Gaya #

Masalah ujinya adalah kalkulator harga dengan aturan yang bisa bertambah: diskon member 10%, diskon pembelian besar (5% untuk subtotal >= 500000), dan potongan biaya penanganan tetap 2500. Konteks pembeli dikirim sebagai array, dan untuk kasus uji kita pakai subtotal 600000 dengan pembeli member. Kedua versi wajib menghasilkan angka yang sama persis.

Versi Fungsional #

Pada gaya fungsional, aturan disimpan sebagai data — array yang berisi closure bertanda tangan seragam:

php
<?php

// Aturan diskon sebagai data: setiap aturan adalah pure function
// dengan tanda tangan yang sama — (subtotal, konteks) => potongan.
$aturanDiskon = [
    'diskon_member' => fn (float $subtotal, array $konteks): float
        => $konteks['member'] ? $subtotal * 0.10 : 0.0,
    'diskon_pembelian_besar' => fn (float $subtotal, array $konteks): float
        => $subtotal >= 500000 ? $subtotal * 0.05 : 0.0,
    'biaya_penanganan' => fn (float $subtotal, array $konteks): float
        => 2500.0,
];

function hitungTotalFungsional(float $subtotal, array $aturan, array $konteks): float
{
    $totalPotongan = array_sum(array_map(
        fn (callable $satuAturan): float => $satuAturan($subtotal, $konteks),
        $aturan
    ));
    return $subtotal - $totalPotongan;
}

$konteksPembeli = ['member' => true];
echo hitungTotalFungsional(600000, $aturanDiskon, $konteksPembeli) . "\n";

Output yang Dihasilkan:

text
507500

Mari kita bedah strukturnya:

  1. $aturanDiskon hanyalah array asosiatif; kuncinya dokumentasi, nilainya closure (subtotal, konteks) => potongan — semua aturan sepakat pada satu bentuk.
  2. hitungTotalFungsional() tidak mengenal satu pun aturan spesifik; fungsi ini menjalankan seluruh aturan lewat array_map(), menjumlahkan potongannya dengan array_sum(), lalu mengurangkan hasilnya dari subtotal.
  3. Menambah aturan baru berarti menambah satu baris ke array — tanpa menyentuh mesin penghitungnya, dan tanpa autoloading, instance, maupun berkas baru.

Versi Berorientasi Objek #

Versi OOP menukar “array of closures” menjadi kontrak interface yang diimplementasi class-class kecil:

php
<?php

interface AturanDiskon
{
    /** Potongan (bisa nol) untuk subtotal dan konteks pembeli. */
    public function hitungPotongan(float $subtotal, array $konteks): float;
}

final class DiskonMember implements AturanDiskon
{
    public function hitungPotongan(float $subtotal, array $konteks): float
    {
        return $konteks['member'] ? $subtotal * 0.10 : 0.0;
    }
}

final class DiskonPembelianBesar implements AturanDiskon
{
    public function hitungPotongan(float $subtotal, array $konteks): float
    {
        return $subtotal >= 500000 ? $subtotal * 0.05 : 0.0;
    }
}

final class BiayaPenanganan implements AturanDiskon
{
    public function hitungPotongan(float $subtotal, array $konteks): float
    {
        return 2500.0;
    }
}

final class KalkulatorHarga
{
    /** @param AturanDiskon[] $daftarAturan */
    public function __construct(private readonly array $daftarAturan) {}

    public function hitungTotal(float $subtotal, array $konteks): float
    {
        $totalPotongan = 0.0;
        foreach ($this->daftarAturan as $aturan) {
            $totalPotongan += $aturan->hitungPotongan($subtotal, $konteks);
        }
        return $subtotal - $totalPotongan;
    }
}

$kalkulator = new KalkulatorHarga([
    new DiskonMember(),
    new DiskonPembelianBesar(),
    new BiayaPenanganan(),
]);

echo $kalkulator->hitungTotal(600000, ['member' => true]) . "\n";

Output yang Dihasilkan:

text
507500

Perhatikan strukturnya:

  1. AturanDiskon menetapkan kontrak formal: siapa pun yang ingin menjadi aturan wajib menyediakan hitungPotongan().
  2. Setiap aturan menjadi class sendiri — DiskonMember, DiskonPembelianBesar, BiayaPenanganan — masing-masing bisa ditemukan lewat autocomplete IDE, diuji terpisah, dan diberi phpdoc atau dependensi bila aturannya rumit.
  3. KalkulatorHarga memiliki daftar aturan melalui constructor (dependency yang tersimpan sebagai state), lalu menjalankannya satu per satu.

Kedua versi menghasilkan 507500 (potongan 60000 + 30000 + 2500 = 92500 dari 600000). Versi fungsional memakai sekitar dua puluh lima baris; versi OOP hampir dua kali lipatnya. Angka baris bukan segalanya — tabel berikut merangkum tradeoff yang sesungguhnya.

Perbandingan yang Jujur #

AspekGaya fungsionalGaya berorientasi objek
Penyimpanan statusmengalir sebagai argumen; tidak tersimpantersimpan dan dikelola di dalam object
Pengujianpanggil fungsi, bandingkan hasil — tanpa persiapaninstansiasi ringan, lalu uji tiap method
Discoverabilitymencari nama fungsi di namespaceinterface + autocomplete + type system
Ekstensi aturantambah closure ke arraytulis class implements interface
Boilerplateminimlebih panjang (deklarasi class/method)
Kinerjaserupa untuk kasus seperti iniserupa — perbedaannya nyaris tak terukur

Baris terakhir layak ditegaskan: untuk kasus seperti ini, keduanya sama-sama cepat. Panggilan closure memang sedikit lebih mahal daripada loop murni, tetapi di aplikasi nyata hampir seluruh waktu eksekusi habis di I/O — database, jaringan, berkas — bukan di pilihan paradigma. Memilih gaya karena mitos kinerja adalah keputusan yang diambil dengan data yang salah.

Budaya PHP: Keduanya Hidup Berdampingan #

Dari awal sejarahnya, PHP memang dwitunggal. Ratusan fungsi global seperti array_map() yang menerima callback adalah DNA fungsional yang hadir jauh sebelum istilahnya populer; sementara model object-nya tumbuh dari PHP 4, matang di PHP 5, dan terus kaya hingga property hooks di PHP 8.4. Framework modern memperlihatkan campuran ini dengan terang: koleksi Laravel merangkai ->filter()->map()->reduce() berbasis method chaining di atas class, dan Symfony memakai closure luas untuk konfigurasi serta callback di dalam arsitektur yang sepenuhnya berorientasi objek. Jadi di ekosistem PHP, pertanyaan “fungsional atau OOP?” kurang tepat; pertanyaan yang tepat adalah “bagian kode yang mana paling cocok dikerjakan secara fungsional?”

Mencampur Secara Pragmatis #

Pola campuran yang paling sering kita temui di codebase modern: value object untuk bentuk data, pure function untuk logika. Perhatikan contoh berikut:

php
<?php

readonly class Pesanan
{
    public function __construct(
        public string $nomor,
        public float $subtotal,
        public bool $member,
    ) {}
}

// Pure function yang bekerja di atas value object.
function totalPesanan(Pesanan $pesanan): float
{
    $diskon = $pesanan->member ? $pesanan->subtotal * 0.10 : 0.0;
    return $pesanan->subtotal - $diskon;
}

$pesananBudi = new Pesanan('INV-2026-001', 400000, true);
$pesananSiti = new Pesanan('INV-2026-002', 400000, false);

echo totalPesanan($pesananBudi) . "\n";
echo totalPesanan($pesananSiti) . "\n";

Output yang Dihasilkan:

text
360000
400000

Class Pesanan membawa manfaat sisi objek: properti bertipe dan beku (readonly, sesuai Chapter Immutability ), validasi di constructor, dan identitas yang dikenali type system. Fungsi totalPesanan() membawa manfaat sisi fungsional: murni, satu tanggung jawab, dan bisa diuji dengan satu baris assertion. Pembagian kerja seperti ini bisa diskalakan: OOP untuk struktur dan siklus hidup (service, repository, controller), gaya fungsional untuk transformasi data dan aturan bisnis di dalamnya — persis pola functional core, imperative shell yang kita bicarakan di awal section.

Panduan Memilih #

Sebagai penutup, berikut panduan praktis memilih pendekatan untuk kasus sehari-hari:

  • Transformasi data lurus (array ke array, normalisasi, format teks) — pipeline fungsional map/filter/reduce.
  • Aturan atau perhitungan tanpa status — pure function; jadikan array of callables bila aturannya bertumpuk.
  • Status dan siklus hidup (koneksi database, transaksi, session) — object, karena encapsulation benar-benar membantu di sini.
  • Kontrak lintas modul yang dikonsumsi banyak tim — interface dan class, agar autocomplete serta type checker bekerja untuk pembaca.
  • Helper kecil sekali pakai — fungsi biasa; angkat ke class hanya ketika ia butuh dependensi atau struktur.

Ringkasan #

Kedua gaya menyelesaikan masalah kalkulator harga yang sama dengan hasil identik: versi fungsional ringkas dengan aturan sebagai data, versi OOP lebih panjang namun kaya kontrak dan discoverability. Tradeoff keduanya nyata namun tidak saling menghapuskan — status terkelola lebih natural di object, logika murni lebih natural di fungsi, dan ekosistem PHP sendiri hidup dengan mencampur keduanya. Dengan selesainya perbandingan ini, perjalanan Section Functional Programming pun tuntas: kita kini memilih gaya berdasarkan kecocokan masalah, bukan keterikatan pada satu kamp.

Referensi #