Sembilan belas chapter sebelumnya memperlengkapi kita dengan class, inheritance, interface, trait, composition, magic method, hingga late static binding — dan sekarang saatnya mengajukan pertanyaan yang paling sering dilupakan: perlukah semua ini? Jawaban jujurnya adalah: tergantung masalahnya. OOP membayar dirinya sendiri dengan bunga pada jenis masalah tertentu, dan menjadi pajak murni pada jenis masalah lain. Bab ini sengaja tidak memperkenalkan fitur baru; fokus kita adalah melatih penilaian — membandingkan gaya prosedural dan OOP pada masalah yang sama, menghitung biaya abstraksi, lalu menutup dengan daftar pertanyaan yang bisa dipakai setiap kali Anda memilih gaya untuk kode berikutnya.
Kekuatan OOP: Kapan Object Membayar Diri Sendiri #
Empat situasi berikut adalah tempat OOP paling konsisten memberi keuntungan nyata:
- State yang punya aturan main (invariant). Saldo rekening tidak boleh negatif, nomor pesanan harus unik, stok tidak boleh kurang dari nol. Class mampu menempatkan state dan aturannya dalam satu tempat terkunci, seperti akan kita buktikan di bagian Invariant .
- Banyak varian perilaku dengan bentuk panggilan sama. Pengiriman standar, kilat, internasional — semuanya dipanggil sebagai
kirim(). Polimorfisme lewat interface membuat kode pemanggil tidak perlu tahu varian mana yang sedang bekerja, persis seperti contoh di chapter Object Composition . - Batas kerja tim. Visibility
private/protected, kontrak interface, dan property bertipe membuat tim bisa berbagi kode tanpa saling menginjak punggung: selama kontrak dijaga, implementasi boleh berubah. - Library dan package reusable. Ketika kode dikonsumsi pihak lain lewat Composer, class dengan method publik yang jelas adalah API yang bisa didokumentasikan, di-version, dan dikelola secara disiplin — jauh lebih mudah daripada sekumpulan fungsi global.
Kapan OOP Justru Menjadi Beban #
Di sisi lain, sejumlah situasi membuat OOP berubah menjadi upacara tanpa makna. Skrip sekali pakai — migrasi data, perapian berkas, laporan ad-hoc — tidak butuh kontrak jangka panjang. Transformasi murni dari input ke output (format teks, konversi satuan, filter array) lebih jernih ditulis sebagai fungsi, apalagi arrow function satu baris. Glue code yang hanya memanggil tiga fungsi berurutan juga tidak memerlukan class pembungkus.
Biayanya nyata: setiap class adalah satu berkas baru yang harus dibuka penulis dan pembacanya; setiap lapisan abstraksi menambah satu lompatan saat menelusuri alur program; dan pembaca yang hanya ingin tahu “bagaimana diskon dihitung?” harus menyisihkan waktu memahami struktur sebelum menemukan jawabannya. Kode prosedural pendek bisa dipahami dari atas ke bawah dalam satu bacaan — keunggulan yang tidak boleh diremehkan hanya karena gaya tersebut terlihat “jadul”.
Masalah yang Sama, Ditulis dengan Dua Gaya #
Agar perbandingannya adil, mari selesaikan satu masalah yang sama dua kali: menghitung subtotal, diskon, dan total dari daftar belanja. Versi proseduralnya memakai tiga fungsi kecil di atas array:
<?php
function hitungSubtotal(array $barang): int
{
$subtotal = 0;
foreach ($barang as $item) {
$subtotal += $item['harga'] * $item['jumlah'];
}
return $subtotal;
}
function hitungDiskon(int $subtotal): int
{
return $subtotal >= 500000 ? intdiv($subtotal, 10) : 0;
}
function formatRupiah(int $nominal): string
{
return "Rp" . number_format($nominal, 0, ",", ".");
}
$barang = [
['nama' => 'Kopi Torabika', 'harga' => 25000, 'jumlah' => 4],
['nama' => 'Gula Pasir', 'harga' => 15000, 'jumlah' => 2],
['nama' => 'Laptop', 'harga' => 7500000, 'jumlah' => 1],
];
$subtotal = hitungSubtotal($barang);
$diskon = hitungDiskon($subtotal);
echo "Subtotal: " . formatRupiah($subtotal) . "\n";
echo "Diskon: " . formatRupiah($diskon) . "\n";
echo "Total: " . formatRupiah($subtotal - $diskon) . "\n";Output yang Dihasilkan:
Subtotal: Rp7.630.000
Diskon: Rp763.000
Total: Rp6.867.000Sekarang masalah yang persis sama dalam gaya OOP — satu class yang memegang daftar barang sebagai state-nya sendiri:
<?php
final class KeranjangBelanja
{
private array $barang = [];
public function tambah(string $nama, int $harga, int $jumlah): void
{
$this->barang[] = [
'nama' => $nama,
'harga' => $harga,
'jumlah' => $jumlah,
];
}
public function subtotal(): int
{
$subtotal = 0;
foreach ($this->barang as $item) {
$subtotal += $item['harga'] * $item['jumlah'];
}
return $subtotal;
}
public function diskon(): int
{
return $this->subtotal() >= 500000 ? intdiv($this->subtotal(), 10) : 0;
}
public function total(): int
{
return $this->subtotal() - $this->diskon();
}
}
function formatRupiah(int $nominal): string
{
return "Rp" . number_format($nominal, 0, ",", ".");
}
$keranjang = new KeranjangBelanja();
$keranjang->tambah('Kopi Torabika', 25000, 4);
$keranjang->tambah('Gula Pasir', 15000, 2);
$keranjang->tambah('Laptop', 7500000, 1);
echo "Subtotal: " . formatRupiah($keranjang->subtotal()) . "\n";
echo "Diskon: " . formatRupiah($keranjang->diskon()) . "\n";
echo "Total: " . formatRupiah($keranjang->total()) . "\n";Output yang Dihasilkan:
Subtotal: Rp7.630.000
Diskon: Rp763.000
Total: Rp6.867.000Kedua versi mencetak tiga baris yang persis sama, dan di ukuran sekecil ini keduanya sama-sama mudah dibaca — itulah pengamatan yang penting: pada masalah kecil, pilihan gaya bukanlah soal benar atau salah. Perbedaannya baru terasa ketika kebutuhan bertumbuh, seperti dirangkum tabel berikut:
| Aspek | Versi prosedural | Versi OOP |
|---|---|---|
| Panjang kode | Lebih ringkas | Bertambah karena deklarasi class |
| Tempat tinggal data | Array $barang dipegang pemanggil, diteruskan antar fungsi | Property $barang hidup di dalam object |
| Aturan diskon berubah per jenis pelanggan | Fungsi baru + parameter tambahan merambat ke semua pemanggil | Varian class baru, pemanggil lama tidak tersentuh |
| Banyak keranjang hidup bersamaan | Tiap pemanggil mengelola array-nya sendiri | Tiap new KeranjangBelanja() adalah unit terpisah dan beridentitas |
Jika toko ini kelak melayani member, reseller, dan korporat dengan aturan diskon berbeda, versi OOP tinggal memecahkan lewat polimorfisme tanpa menyentuh kode pemanggil. Jika skrip ini hanya sekadar laporan mingguan yang dibuang setelah dipakai, versi prosedural menang karena tidak menagih biaya struktur di muka.
Invariant: Saat Class Benar-Benar Menang #
Ada satu situasi di mana keunggulan OOP bukan sekadar kenyamanan, melainkan kebenaran program: ketika data harus mematuhi aturan yang tidak boleh dilanggar siapa pun. Bayangkan saldo rekening yang dikelola sebagai angka bebas di array — kode mana pun bisa menulisnya menjadi negatif tanpa peringatan. Class menutup celah itu dengan menempatkan aturan di setiap pintu masuk:
<?php
final class RekeningBank
{
private int $saldo;
public function __construct(int $saldoAwal)
{
if ($saldoAwal < 0) {
throw new InvalidArgumentException("Saldo awal tidak boleh negatif");
}
$this->saldo = $saldoAwal;
}
public function tarik(int $jumlah): int
{
if ($jumlah <= 0) {
throw new InvalidArgumentException("Jumlah penarikan harus positif");
}
if ($jumlah > $this->saldo) {
throw new RuntimeException("Saldo tidak cukup");
}
$this->saldo -= $jumlah;
return $this->saldo;
}
public function setor(int $jumlah): int
{
if ($jumlah <= 0) {
throw new InvalidArgumentException("Jumlah setoran harus positif");
}
$this->saldo += $jumlah;
return $this->saldo;
}
public function saldoSekarang(): int
{
return $this->saldo;
}
}
$rekening = new RekeningBank(500000);
$rekening->tarik(150000);
$rekening->setor(50000);
echo "Saldo akhir: Rp" . $rekening->saldoSekarang() . "\n";
try {
$rekening->tarik(999999);
} catch (RuntimeException $e) {
echo "Ditolak: " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
Saldo akhir: Rp400000
Ditolak: Saldo tidak cukupProperty $saldo diprivatkan sehingga tidak ada jalan mengubahnya kecuali lewat tarik() dan setor() — dan keduanya memeriksa aturan main sebelum menyentuh saldo. Percobaan menarik Rp999.999 ditolak keras dengan exception, bukan diam-diam menghasilkan saldo negatif. Invariant “saldo tidak pernah negatif” kini dijaga oleh bahasa itu sendiri, dan setiap tes yang mengetes class ini otomatis mengetes aturan tersebut. Inilah alasan paling kuat untuk menginvestasikan sebuah class: bukan agar kode terlihat modern, melainkan agar aturan bisnis tidak bisa dilanggar bahkan oleh kode masa depan yang belum ditulis.
Biaya Abstraksi yang Terlalu Dini #
Kebalikan dari invariant adalah abstraksi yang dibangun sebelum masalahnya nyata. Cuplikan berikut memperlihatkan wujud over-engineering — tiga lapis struktur untuk sebuah penjumlahan:
<?php
// Over-engineering: tiga lapis struktur untuk penjumlahan dua angka
interface PenambahInterface
{
}
abstract class PenambahAbstrak implements PenambahInterface
{
}
final class PenambahStandar extends PenambahAbstrak
{
public function tambah(int $a, int $b): int
{
return $a + $b;
}
}
$hasil = (new PenambahStandar())->tambah(2, 3);
// Versi yang memadai untuk kebutuhan sesederhana ini:
function tambah(int $a, int $b): int
{
return $a + $b;
}
$total = tambah(2, 3);Tidak ada varian kedua yang akan datang, tidak ada tim kedua yang akan mengganti implementasi, dan tidak ada aturan yang perlu dijaga — seluruh menara interface-abstract-class hanya menunda pembaca sampai menemukan baris return $a + $b;. Abstraksi adalah investasi: ia berbunga ketika variasi yang diantisipasi benar-benar datang, dan menjadi utang ketika tidak. Karena itu urutan yang sehat adalah menulis versi paling sederhana yang berfungsi, lalu mengekstrak class atau interface pada saat rasa sakitnya terasa — bukan pada saat membayangkan rasa sakitnya.
Daftar Pertanyaan Sebelum Memilih #
Berikut enam pertanyaan yang bisa dijadikan daftar periksa setiap kali memilih gaya untuk kode baru. Jawab dengan cepat dan jujur, lalu biarkan jawabannya menentukan arah:
- Apakah data punya aturan yang harus selalu terjaga (invariant) seperti saldo, stok, atau status pesanan?
- Apakah ada beberapa varian perilaku dengan bentuk panggilan yang sama (kandidat polimorfisme)?
- Apakah state perlu hidup melintasi beberapa operasi — misalnya keranjang yang terus terisi antar langkah?
- Apakah kode ini akan dikonsumsi pihak lain sebagai library atau package?
- Sebaliknya: apakah program hanyalah transformasi input ke output tanpa state yang tersimpan?
- Apakah skrip ini sekali pakai dan akan dibuang setelah tujuannya selesai?
Mayoritas jawaban “ya” pada empat pertanyaan pertama menunjuk ke OOP; jawaban “ya” pada dua terakhir menunjuk ke kode prosedural. Wilayah abu-abu di antaranya adalah ruang penilaian — dan untuk wilayah itu, mulailah prosedural, karena mengekstrak class dari fungsi yang sudah teruji jauh lebih mudah daripada membongkar class yang ternyata tidak diperlukan.
Ringkasan #
OOP membayar dirinya ketika ada invariant yang dijaga, varian perilaku yang dipolimorfisme-kan, batas kerja tim yang perlu dikunci, atau API yang dikonsumsi pihak lain. Ia menjadi beban pada skrip sekali pakai, transformasi murni, dan kode lem yang tipis — di sana fungsi prosedural yang pendek dan lugas menang. Perbandingan dua gaya pada masalah yang sama memperlihatkan bahwa perbedaannya bukan benar-salah, melainkan kecocokan dengan arah pertumbuhan program. Dengan dasar OOP yang kini lengkap, kita siap melangkah ke bagian berikutnya dari perjalanan ini: Section Advanced Object Model , tempat model object PHP modern — enum, readonly class, hingga property hooks — menunggu untuk dibedah.