Setiap kali jenis pelanggan baru masuk ke sistem, kita sering terpaksa membuka kembali fungsi diskon yang sudah stabil, menambah satu cabang case, lalu mengulang seluruh pengujian karena kode yang sama ikut berubah. Open/Closed Principle (OCP) — huruf “O” dalam SOLID — hadir justru untuk mencegah situasi semacam itu. Prinsip ini menyatakan bahwa sebuah modul seharusnya terbuka untuk ekstensi, tetapi tertutup untuk modifikasi. Pada chapter ini kita akan mengenali bentuk pelanggarannya yang paling umum, melakukan refactor menuju polimorfisme, dan membahas kapan modifikasi justru masih wajar dilakukan.
Apa Itu Open/Closed Principle #
Istilah ini pertama kali diperkenalkan oleh Bertrand Meyer pada 1988, lalu dipopulerkan Robert C. Martin sebagai komponen SOLID (lihat chapter Solid Principles ). Maknanya memiliki dua sisi yang saling melengkapi: terbuka untuk ekstensi berarti perilaku baru tetap bisa ditambahkan ketika kebutuhan berubah, sedangkan tertutup untuk modifikasi berarti kode sumber yang sudah teruji tidak perlu diedit hanya untuk menambah perilaku tersebut.
Kedengarannya kontradiktif — bagaimana mungkin menambah perilaku tanpa mengubah kode? Kuncinya adalah memindahkan titik variasi dari isi fungsi ke struktur class. Alatnya adalah interface dan polimorfisme yang sudah kita kenal di chapter Interfaces
dan Inheritance
. Daripada satu fungsi yang bercabang-cabang, kita siapkan kontrak tunggal lalu menulis satu class per variasi.
Bau Kode Switch Berdasarkan Tipe #
Bentuk pelanggaran OCP yang paling sering dijumpai adalah switch atau if-else yang memilah perilaku berdasarkan “tipe” sebuah nilai, biasanya string atau konstanta. Perhatikan fungsi perhitungan diskon berikut:
<?php
function hitungDiskon(string $tipePelanggan, int $totalBelanja): int
{
switch ($tipePelanggan) {
case "reguler":
return 0;
case "silver":
return (int) floor($totalBelanja * 0.05);
case "gold":
return (int) floor($totalBelanja * 0.10);
default:
return 0;
}
}
echo hitungDiskon("reguler", 200000), "\n";
echo hitungDiskon("silver", 200000), "\n";
echo hitungDiskon("gold", 200000), "\n";Output yang Dihasilkan:
0
10000
20000Mari kita telaah kode di atas:
- Baris 3: parameter
$tipePelangganberupa string. Seluruh daftar jenis pelanggan harus diketahui oleh fungsi ini. - Baris 5-13: setiap cabang
casememetakan satu tipe ke satu aturan diskon. Semua variasi menumpuk di satu tempat. - Baris 17-19: pemanggil cukup melempar string dan angka; hasilnya konsisten untuk tiga tipe tersebut.
Kode ini bekerja dengan benar, tetapi menyimpan masalah struktural. Ketika manajemen menambah tipe platinum, kita harus mengedit fungsi ini — dan bila ada lima fungsi serupa (hitung diskon, tentukan batas cicilan, susun label harga), kelima-limanya harus dibuka satu per satu. Setiap perubahan pada kode yang sudah teruji membawa risiko regresi, dan cabang yang terlupakan hanya ketahuan saat runtime.
Refactor Menuju Polimorfisme #
Langkah refactornya: angkat setiap cabang menjadi class, lalu ikat semuanya dengan satu interface. Fungsi konsumen cukup menerima kontrak tersebut dan tidak pernah tahu variasi apa yang ada di dalamnya.
<?php
interface Diskon
{
public function hitung(int $totalBelanja): int;
}
final class DiskonReguler implements Diskon
{
public function hitung(int $totalBelanja): int
{
return 0;
}
}
final class DiskonSilver implements Diskon
{
public function hitung(int $totalBelanja): int
{
return (int) floor($totalBelanja * 0.05);
}
}
final class DiskonGold implements Diskon
{
public function hitung(int $totalBelanja): int
{
return (int) floor($totalBelanja * 0.10);
}
}
function tampilkanDiskon(Diskon $diskon, int $totalBelanja): void
{
echo get_class($diskon), ": ", $diskon->hitung($totalBelanja), "\n";
}
tampilkanDiskon(new DiskonReguler(), 200000);
tampilkanDiskon(new DiskonSilver(), 200000);
tampilkanDiskon(new DiskonGold(), 200000);Output yang Dihasilkan:
DiskonReguler: 0
DiskonSilver: 10000
DiskonGold: 20000Perubahan pentingnya terletak pada empat titik:
- Baris 3-6:
interface Diskonmenjadi kontrak tunggal — satu methodhitung()yang menerima total belanja dan mengembalikan angka diskon. - Baris 8-30: setiap tipe pelanggan menjadi class tersendiri yang mengimplementasikan kontrak tersebut. Aturan tiap tipe terisolasi di berkasnya sendiri.
- Baris 32-35: fungsi
tampilkanDiskon()bertipe parameterDiskon, bukan string. Fungsi ini tertutup — isinya tidak akan pernah berubah lagi ketika tipe pelanggan baru ditambahkan. - Baris 37-39: pemanggil memilih class konkret saat memanggil; setelah itu semua berjalan lewat kontrak.
Hasil kedua versi identik, tetapi struktur risikonya berbeda total:
| Aspek | Versi switch | Versi polimorfisme |
|---|---|---|
| Menambah tipe baru | edit fungsi (dan semua cabang serupa di tempat lain) | tulis satu class baru |
| Risiko regresi | tinggi — kode teruji ikut diubah | rendah — kode lama tak tersentuh |
| Pengujian ulang | seluruh fungsi | hanya class baru |
| Pertumbuhan kompleksitas | cabang menumpuk di satu fungsi | tetap datar, satu class per variasi |
Ekstensi Tanpa Menyentuh Kode Lama #
Sekarang kita buktikan klaim utama OCP. Manajemen menambah tipe platinum dengan aturan: diskon 15% plus bonus Rp25.000 untuk transaksi di atas Rp500.000. Perhatikan bahwa kita hanya menulis class baru — kode lama tidak diubah satu karakter pun:
<?php
interface Diskon
{
public function hitung(int $totalBelanja): int;
}
final class DiskonGold implements Diskon
{
public function hitung(int $totalBelanja): int
{
return (int) floor($totalBelanja * 0.10);
}
}
final class DiskonPlatinum implements Diskon
{
public function hitung(int $totalBelanja): int
{
$diskonDasar = (int) floor($totalBelanja * 0.15);
$bonus = $totalBelanja >= 500000 ? 25000 : 0;
return $diskonDasar + $bonus;
}
}
function tampilkanDiskon(Diskon $diskon, int $totalBelanja): void
{
echo get_class($diskon), ": ", $diskon->hitung($totalBelanja), "\n";
}
tampilkanDiskon(new DiskonGold(), 200000);
tampilkanDiskon(new DiskonPlatinum(), 200000);
tampilkanDiskon(new DiskonPlatinum(), 600000);Output yang Dihasilkan:
DiskonGold: 20000
DiskonPlatinum: 30000
DiskonPlatinum: 115000Class DiskonPlatinum membawa aturan berlapis — diskon dasar 15% dihitung dengan floor(), lalu bonus Rp25.000 diberikan hanya ketika total belanja minimal Rp500.000. Kompleksitas ini kini terkurung di satu class yang bisa diuji terpisah. Fungsi tampilkanDiskon() menerima objek baru tersebut tanpa perubahan apa pun karena DiskonPlatinum tunduk pada kontrak Diskon yang sama — inilah wujud nyata “terbuka untuk ekstensi, tertutup untuk modifikasi”.
Konfigurasi Tanpa Percabangan #
Pada aplikasi nyata, pemetaan “tipe pelanggan → strategi diskon” biasanya datang dari konfigurasi atau database, bukan hard-coded. Kita bisa menyimpan daftar class yang diizinkan dalam satu array, lalu menginstansiasinya secara dinamis:
<?php
interface Diskon
{
public function hitung(int $totalBelanja): int;
}
final class DiskonSilver implements Diskon
{
public function hitung(int $totalBelanja): int
{
return (int) floor($totalBelanja * 0.05);
}
}
final class DiskonGold implements Diskon
{
public function hitung(int $totalBelanja): int
{
return (int) floor($totalBelanja * 0.10);
}
}
$petaStrategi = [
"silver" => DiskonSilver::class,
"gold" => DiskonGold::class,
];
foreach ($petaStrategi as $tipe => $namaClass) {
$diskon = new $namaClass();
echo "$tipe: ", $diskon->hitung(200000), "\n";
}Output yang Dihasilkan:
silver: 10000
gold: 20000Array $petaStrategi memetakan kode tipe ke nama class melalui konstanta ::class. Ekspresi new $namaClass() menginstansiasi class yang namanya tersimpan di variabel, sehingga menambah tipe baru cukup dengan menambah satu baris entri — percabangan switch menghilang sama sekali. Pendekatan ini juga nyaman digunakan bersama file konfigurasi yang di-parse saat aplikasi booting.
Kapan Modifikasi Masih Wajar #
OCP bukan dogma yang memaksa semua kode kebal terhadap perubahan. Memperbaiki bug jelas menuntut kita menyentuh kode yang salah. Perubahan pada bagian internal sebuah class — misalnya mengganti cara DiskonSilver membulatkan angka — juga sah selama kontraknya (input, output, efek samping) tidak berubah; pemanggil tidak terdampak.
Yang perlu diwaspadai adalah dua ekstrem sekaligus. Ekstrem pertama: biarkan switch tumbuh sampai puluhan cabang karena “belum waktunya refactor”. Ekstrem kedua: membangun lapisan abstraksi spekulatif — interface plus lima class — untuk sesuatu yang variasinya belum nyata ada. Abstraksi juga punya biaya: lebih banyak berkas, lebih banyak lompatan saat membaca kode, dan fleksibilitas yang tidak pernah dipakai.
Ringkasan #
Open/Closed Principle memindahkan titik variasi dari cabang switch di dalam fungsi ke struktur class yang diikat interface. Kode konsumen ditulis sekali menghadap kontrak, lalu perilaku baru datang dari class baru — bukan dari mengedit kode yang sudah teruji. Struktur seperti ini hanya aman bila setiap implementasi benar-benar bisa dipertukarkan tanpa mengejutkan pemanggilnya. Syarat tersebut adalah wilayah prinsip SOLID berikutnya: Liskov Substitution.