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
<?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
<?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:

text
ℹ️ [2026-01-15 08:00:00] layanan menyala
[2026-01-15 08:00:00] ℹ️ layanan menyala
ℹ️ [2026-01-15 08:00:00] LAYANAN MENYALA

Mari kita bedah lapisannya:

  1. Baris 3-6: interface PencatatLog adalah kontrak tunggal yang dipegang komponen dasar, semua dekorator, dan pemanggil sekaligus.
  2. Baris 8-14: PencatatTerminal adalah komponen dasar yang benar-benar mencetak — satu-satunya yang tahu cara menulis ke layar.
  3. Baris 16-21: DekoratorPencatat adalah induk semua dekorator. Berkat constructor promotion PHP 8, seluruh persiapannya hanya empat baris; property readonly menjamin objek yang dibungkus tidak bisa ditukar di tengah rantai.
  4. Baris 23-53: tiga dekorator kecil, masing-masing menambah satu perilaku lalu melanjutkan ke $this->dibungkus. PencatatBerwaktu menerima DateTimeImmutable lewat constructor dan hanya menambahkan format waktu di depan pesan.
  5. 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 PencatatKapital di 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
<?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:

text
Pesan yang sampai : [2026-01-15 08:00:00] uji dekorator berwaktu
Jumlah panggilan  : 1

PencatatMataMata 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:

AspekInheritance (subclass)Decorator
Kombinasi fitursatu subclass per kombinasi (ledakan 2^n)n dekorator, ditumpuk sesukanya
Waktu penetapansejak lahir, melekat pada classper instance, bahkan setelah objek dibuat
Menambah perilaku ke objek yang sudah adatidak bisacukup bungkus ulang
Kecocokan alamivarian tetap yang selalu aktiffitur 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
<?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:

text
[log] 200: halaman /beranda
401: belum login

Strukturnya 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.

Referensi #