Adapter Pattern menyatukan dua antarmuka yang berbeda bentuk; Decorator Pattern menghadapi kasus yang sebaliknya — antarmuka sudah cocok, tetapi perilakunya ingin diperkaya lapis demi lapis. Logger adalah contoh yang paling sering dijumpai: kadang pesan perlu stempel waktu, kadang perlu huruf kapital, kadang perlu keduanya sekaligus. Chapter ini membahas cara membungkus objek dengan objek sejenis untuk menambah perilaku, menjaga transparansi interface, menguji dekorator lewat objek mata-mata, memilih antara decorator dan inheritance, lalu sekilas melihat middleware HTTP yang dibangun di atas gagasan yang sama.
Masalah: Ledakan Kombinasi Subclass #
Cara pertama yang terlintas untuk memperkaya sebuah class biasanya adalah pewarisan: buat subclass untuk tiap variasi perilaku. Dua fitur pertama memang terlihat wajar, lalu kombinasinya memaksa subclass ketiga muncul:
<?php
class PencatatDasar
{
public function tulis(string $pesan): void
{
echo $pesan, "\n";
}
}
class PencatatBerwaktu extends PencatatDasar
{
public function tulis(string $pesan): void
{
parent::tulis("[waktu] $pesan");
}
}
class PencatatKapital extends PencatatDasar
{
public function tulis(string $pesan): void
{
parent::tulis(mb_strtoupper($pesan));
}
}
// Kombinasi keduanya terpaksa menjadi subclass ketiga...
class PencatatBerwaktuKapital extends PencatatBerwaktu
{
public function tulis(string $pesan): void
{
parent::tulis(mb_strtoupper($pesan));
}
}
// ...dan itu baru dua fitur dari tiga yang direncanakan
Matematikanya kejam: tiga fitur opsional menghasilkan delapan kombinasi, dan setiap kombinasi adalah satu subclass yang harus didefinisikan, diberi nama, dan dirawat. Pewarisan juga memutuskan segalanya saat objek lahir — sebuah instance PencatatBerwaktuKapital tidak bisa kehilangan atau menambah satu lapis perilaku tanpa lahir ulang sebagai objek lain. Yang kita butuhkan justru cara menyusun perilaku seperti susunan balok: dipasang dan dilepas per objek, bukan dipatri di pohon warisan.
Membungkus Tanpa Mengubah Kontrak #
Decorator Pattern memakai satu aturan sederhana: pembungkus mengimplementasi interface yang sama persis dengan objek yang dibungkusnya. Karena kontraknya tidak berubah, pembungkus bisa dibungkus pembungkus lain, dan pemanggil tidak pernah tahu — juga tidak perlu tahu — berapa lapis yang sedang bekerja:
<?php
interface PencatatLog
{
public function tulis(string $pesan): void;
}
final class PencatatTerminal implements PencatatLog
{
public function tulis(string $pesan): void
{
echo $pesan, "\n";
}
}
abstract class DekoratorPencatat implements PencatatLog
{
public function __construct(protected readonly PencatatLog $dibungkus)
{
}
}
final class PencatatBerwaktu extends DekoratorPencatat
{
public function __construct(
PencatatLog $dibungkus,
private readonly DateTimeImmutable $jam,
) {
parent::__construct($dibungkus);
}
public function tulis(string $pesan): void
{
$waktu = $this->jam->format("Y-m-d H:i:s");
$this->dibungkus->tulis("[$waktu] $pesan");
}
}
final class PencatatKapital extends DekoratorPencatat
{
public function tulis(string $pesan): void
{
$this->dibungkus->tulis(mb_strtoupper($pesan));
}
}
final class PencatatBeremoji extends DekoratorPencatat
{
public function tulis(string $pesan): void
{
$this->dibungkus->tulis("ℹ️ $pesan");
}
}
$jamBeku = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
// Tumpukan pertama: emoji di lapisan terdalam, awalannya paling depan
$emojiTerdalam = new PencatatBerwaktu(
new PencatatBeremoji(new PencatatTerminal()),
$jamBeku
);
$emojiTerdalam->tulis("layanan menyala");
// Tumpukan kedua: posisi ditukar, urutan awalan ikut bertukar
$waktuTerdalam = new PencatatBeremoji(
new PencatatBerwaktu(new PencatatTerminal(), $jamBeku)
);
$waktuTerdalam->tulis("layanan menyala");
// Tumpukan ketiga: tiga lapisan sekaligus
$lengkap = new PencatatBerwaktu(
new PencatatKapital(new PencatatBeremoji(new PencatatTerminal())),
$jamBeku
);
$lengkap->tulis("layanan menyala");Output yang Dihasilkan:
ℹ️ [2026-01-15 08:00:00] layanan menyala
[2026-01-15 08:00:00] ℹ️ layanan menyala
ℹ️ [2026-01-15 08:00:00] LAYANAN MENYALAMari kita bedah lapisannya:
- Baris 3-6: interface
PencatatLogadalah kontrak tunggal yang dipegang komponen dasar, semua dekorator, dan pemanggil sekaligus. - Baris 8-14:
PencatatTerminaladalah komponen dasar yang benar-benar mencetak — satu-satunya yang tahu cara menulis ke layar. - Baris 16-21:
DekoratorPencatatadalah induk semua dekorator. Berkat constructor promotion PHP 8, seluruh persiapannya hanya empat baris; propertyreadonlymenjamin objek yang dibungkus tidak bisa ditukar di tengah rantai. - Baris 23-53: tiga dekorator kecil, masing-masing menambah satu perilaku lalu melanjutkan ke
$this->dibungkus.PencatatBerwaktumenerimaDateTimeImmutablelewat constructor dan hanya menambahkan format waktu di depan pesan. - Baris 57-75: tiga tumpukan yang berbeda dari bahan yang sama. Lapisan terdalam ternyata menulis awalan paling depan: tumpukan pertama menghasilkan emoji sebelum stempel waktu, tumpukan kedua kebalikannya, dan tumpukan ketiga membuktikan bahwa
PencatatKapitaldi tengah tumpukan mengubah seluruh pesan yang melintasinya.
Transparansi Interface dan Kemudahan Menguji #
Karena dekorator mengimplementasi interface yang sama dengan objek yang dibungkusnya, hasil tumpukan apa pun tetap berupa PencatatLog — pemanggil tidak memiliki cara untuk membedakannya, dan itulah inti transparansinya. Transparansi ini juga membuka trik pengujian yang rapi: ganti komponen terdalam dengan objek mata-mata yang merekam, lalu periksa pesan apa yang sampai kepadanya:
<?php
interface PencatatLog
{
public function tulis(string $pesan): void;
}
abstract class DekoratorPencatat implements PencatatLog
{
public function __construct(protected readonly PencatatLog $dibungkus)
{
}
}
final class PencatatBerwaktu extends DekoratorPencatat
{
public function __construct(
PencatatLog $dibungkus,
private readonly DateTimeImmutable $jam,
) {
parent::__construct($dibungkus);
}
public function tulis(string $pesan): void
{
$waktu = $this->jam->format("Y-m-d H:i:s");
$this->dibungkus->tulis("[$waktu] $pesan");
}
}
// Pengganti tiruan: merekam pesan alih-alih mencetaknya
final class PencatatMataMata implements PencatatLog
{
/** @var list<string> */
public array $rekaman = [];
public function tulis(string $pesan): void
{
$this->rekaman[] = $pesan;
}
}
$mataMata = new PencatatMataMata();
$pencatat = new PencatatBerwaktu(
$mataMata,
new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"))
);
$pencatat->tulis("uji dekorator berwaktu");
echo "Pesan yang sampai : ", $mataMata->rekaman[0], "\n";
echo "Jumlah panggilan : ", count($mataMata->rekaman), "\n";Output yang Dihasilkan:
Pesan yang sampai : [2026-01-15 08:00:00] uji dekorator berwaktu
Jumlah panggilan : 1PencatatMataMata pada baris 31-41 mengimplementasi interface yang sama tetapi tidak mencetak apa pun — setiap pesan yang diterimanya ia simpan ke array $rekaman. Baris 43-49 lalu menyusun satu dekorator PencatatBerwaktu dengan mata-mata sebagai komponen terdalam, memanggil tulis() sekali, dan memeriksa dua hal: isi pesan yang sampai sudah memuat stempel waktu, dan jumlah pemanggilan tepat satu. Penglolaan pengujian tanpa membaca keluaran layar membuat pernyataan uji menjadi presisi dan bebas dari kebisingan format.
Decorator atau Inheritance #
Pilihan di antara keduanya bisa dipetakan dengan jelas:
| Aspek | Inheritance (subclass) | Decorator |
|---|---|---|
| Kombinasi fitur | satu subclass per kombinasi (ledakan 2^n) | n dekorator, ditumpuk sesukanya |
| Waktu penetapan | sejak lahir, melekat pada class | per instance, bahkan setelah objek dibuat |
| Menambah perilaku ke objek yang sudah ada | tidak bisa | cukup bungkus ulang |
| Kecocokan alami | varian tetap yang selalu aktif | fitur opsional yang bisa dicampur |
Subclass tetap pilihan yang lebih jujur ketika perilaku benar-benar melekat pada satu varian: KartuKredit implements MetodePembayaran adalah hubungan yang tetap, bukan lapisan yang bisa dilepas. Decorator unggul justru ketika perilakunya opsional dan kombinatorik — format, stempel waktu, prefiks, rate limit, caching — karena tiap lapis hidup di class kecilnya sendiri dan bebas disusun ulang.
Sekilas: Middleware sebagai Rantai Decorator #
Kalau Anda pernah memakai framework PHP modern, Anda sebenarnya sudah lama memakai decorator: middleware HTTP adalah rantai pembungkus di sekeliling handler aplikasi. Versi mainannya dapat kita tulis dalam dua puluh baris:
<?php
interface HandlerHttp
{
public function tangani(string $path): string;
}
abstract class Middleware implements HandlerHttp
{
public function __construct(protected readonly HandlerHttp $berikutnya)
{
}
}
final class MiddlewareAuth extends Middleware
{
public function tangani(string $path): string
{
if ($path === "/admin") {
return "401: belum login";
}
return $this->berikutnya->tangani($path);
}
}
final class MiddlewareLog extends Middleware
{
public function tangani(string $path): string
{
return "[log] " . $this->berikutnya->tangani($path);
}
}
final class HandlerHalaman implements HandlerHttp
{
public function tangani(string $path): string
{
return "200: halaman $path";
}
}
$app = new MiddlewareAuth(new MiddlewareLog(new HandlerHalaman()));
echo $app->tangani("/beranda"), "\n";
echo $app->tangani("/admin"), "\n";Output yang Dihasilkan:
[log] 200: halaman /beranda
401: belum loginStrukturnya persis tumpukan PencatatLog kita: Middleware (baris 8-13) adalah dekorator dasar yang menyimpan handler berikutnya, MiddlewareAuth dan MiddlewareLog menambah perilaku di depannya, dan HandlerHalaman adalah komponen inti. Baris 45-46 memperlihatkan satu sifat penting yang belum muncul di logger: dekorator boleh memutus rantai. Permintaan ke /beranda mengalir melewati autentikasi, diberi awalan log, lalu ditangani halaman; permintaan ke /admin dihentikan di MiddlewareAuth dan tidak pernah mencapai lapisan berikutnya.
Ringkasan #
Decorator Pattern menambah perilaku lewat pembungkus yang mengimplementasi interface yang sama: tiga fitur opsional cukup tiga class kecil alih-alih delapan subclass, tumpukan disusun per objek bahkan setelah objek dibuat, dan transparansi interface membuat pemanggil maupun objek mata-mata tidak bisa membedakan hasil tumpukan dari komponen telanjangnya. Pewarisan tetap menang untuk varian yang melekat permanen, dan middleware HTTP membuktikan pola ini bekerja di jantung framework modern. Pattern berikutnya melangkah lebih jauh dari satu objek yang dibungkus: Observer Pattern menyiarkan kejadian kepada banyak pendengar yang terdaftar, tanpa saling mengenal.