Layanan notifikasi kita berawal dari email saja. Lalu manajemen meminta tambahan sms untuk pesanan penting; dua bulan kemudian, audit meminta setiap pengiriman dicatat ke log. Solusi dengan pewarisan menuntut class NotifikasiEmailSms, NotifikasiEmailLog, NotifikasiEmailSmsLog — tiga opsi saja sudah begini, dan setiap kombinasi baru menggandakan jumlah class. Decorator pattern menyelesaikannya tanpa menyentuh class asal: setiap tambahan perilaku adalah class pembungkus berantarmuka sama, yang bisa ditumpuk sesuai kebutuhan pada saat penciptaan objek. Chapter ini membangun notifikasi bertumpuk lengkap dengan biaya berjenjang, lalu menengok korpus klasik pola ini di pustaka java.io.

Warisan yang Berlipat Dua #

Pewarisan memang bisa menampung kasus email+sms, tetapi biayanya tumbuh eksponensial terhadap jumlah opsi. Enam opsi berarti 64 kombinasi class, dan sebagian besar kombinasi itu nyaris tak terpakai. Masalah yang lebih halus: class dasar NotifikasiEmail harus diubah setiap kali ada perilaku yang ingin “disisipkan” di tengah alurnya — melanggar prinsip open/closed yang sudah kita bahas di chapter SOLID.

Dekorator menawarkan komposisi. Perilaku tambahan tidak disuntikkan ke dalam class dasar, melainkan dibungkus di sekelilingnya: pembungkus menyetujui kontrak antarmuka yang sama, menyimpan objek yang dibungkus, lalu mengerjakan tugasnya sebelum atau sesudah meneruskan pemanggilan. Karena pembungkus juga memenuhi antarmuka, pembungkus bisa dibungkus lagi — sifat inilah yang membuat jumlah kombinasi tumbuh linier terhadap jumlah dekorator, bukan eksponensial.

Notifikasi Bertumpuk dalam Kode #

Program berikut memakai satu komponen dasar dan dua dekorator yang bebas ditumpuk, dengan biaya yang bertambah berjenjang di tiap lapisan:

public class NotifikasiBerlapis {
    public static void main(String[] args) {
        // Komponen dasar: hanya email
        Notifikasi dasar = new NotifikasiEmail();
        System.out.println("== email saja ==");
        dasar.kirim("Pesanan sudah dikirim");
        System.out.println("biaya per kirim: Rp" + dasar.biaya());

        // Bungkus dengan sms: perilaku bertambah tanpa mengubah class dasar
        Notifikasi denganSms = new DenganSms(dasar);
        System.out.println("== email + sms ==");
        denganSms.kirim("Pesanan sudah dikirim");
        System.out.println("biaya per kirim: Rp" + denganSms.biaya());

        // Bungkus lagi dengan log: dekorator boleh bertumpuk
        Notifikasi lengkap = new DenganLog(new DenganSms(dasar));
        System.out.println("== email + sms + log ==");
        lengkap.kirim("Pesanan sudah dikirim");
        System.out.println("biaya per kirim: Rp" + lengkap.biaya());
    }
}

// Komponen: antarmuka yang sama untuk objek dasar maupun pembungkusnya
interface Notifikasi {
    void kirim(String pesan);

    long biaya(); // biaya per kiriman dalam rupiah
}

class NotifikasiEmail implements Notifikasi {
    @Override
    public void kirim(String pesan) {
        System.out.println("email : mengirim \"" + pesan + "\"");
    }

    @Override
    public long biaya() {
        return 100;
    }
}

// Dekorator dasar: memegang komponen yang dibungkus dan meneruskan seluruh method
abstract class DekoratorNotifikasi implements Notifikasi {
    protected final Notifikasi dibungkus;

    DekoratorNotifikasi(Notifikasi dibungkus) {
        this.dibungkus = dibungkus;
    }
}

class DenganSms extends DekoratorNotifikasi {
    DenganSms(Notifikasi dibungkus) {
        super(dibungkus);
    }

    @Override
    public void kirim(String pesan) {
        dibungkus.kirim(pesan);
        System.out.println("sms   : mengirim \"" + pesan + "\"");
    }

    @Override
    public long biaya() {
        return dibungkus.biaya() + 500; // biaya berjenjang: sms menambah 500
    }
}

class DenganLog extends DekoratorNotifikasi {
    DenganLog(Notifikasi dibungkus) {
        super(dibungkus);
    }

    @Override
    public void kirim(String pesan) {
        System.out.println("log   : mulai pengiriman \"" + pesan + "\"");
        dibungkus.kirim(pesan);
        System.out.println("log   : pengiriman selesai \"" + pesan + "\"");
    }

    @Override
    public long biaya() {
        return dibungkus.biaya() + 10; // pencatatan murah: hanya 10
    }
}

Output yang Dihasilkan:

text
== email saja ==
email : mengirim "Pesanan sudah dikirim"
biaya per kirim: Rp100
== email + sms ==
email : mengirim "Pesanan sudah dikirim"
sms   : mengirim "Pesanan sudah dikirim"
biaya per kirim: Rp600
== email + sms + log ==
log   : mulai pengiriman "Pesanan sudah dikirim"
email : mengirim "Pesanan sudah dikirim"
sms   : mengirim "Pesanan sudah dikirim"
log   : pengiriman selesai "Pesanan sudah dikirim"
biaya per kirim: Rp610

Mari kita bedah lapisan demi lapisan:

  1. Baris 24-28 (interface Notifikasi): kontrak yang sama untuk komponen dasar dan pembungkus — syarat mutlak dekorator. Tanpa kesamaan antarmuka ini, kode klien tidak bisa menerima hasil pembungkusan secara transparan.
  2. Baris 43-49 (abstract class DekoratorNotifikasi): akar seluruh dekorator. Class ini menyimpan referensi dibungkus dan menyerahkan keputusan “apa yang dikerjakan sebelum, sesudah, atau menggantikan” kepada subclass masing-masing.
  3. Baris 51-66 (class DenganSms): kirim meneruskan pekerjaan lebih dulu lewat dibungkus.kirim(pesan) lalu menambah baris sms — perhatikan urutan output “email” sebelum “sms”. Method biaya menjumlahkan biaya lapisan dalam ditambah 500: harga berjenjang yang terhitung otomatis apa pun susunan tumpukannya.
  4. Baris 68-84 (class DenganLog): dekorator yang bekerja di kedua sisi — mencatat “mulai” sebelum meneruskan dan “selesai” setelahnya. Di main baris 18, DenganLog berada di lapisan terluar, sehingga catatannya mengapit seluruh pengiriman email dan sms, dan total biaya 610 adalah jumlah seluruh lapisan: 100 + 500 + 10.

java.io Korpus Klasik #

Pustaka java.io adalah contoh terbesar dekorator di JDK. BufferedReader tidak tahu membaca dari apa: class tersebut hanya membungkus Reader mana pun dan menambah perilaku buffering; InputStreamReader membungkus InputStream dan menambah terjemahan byte ke karakter. Rantai new BufferedReader(new InputStreamReader(System.in)) persis new DenganLog(new DenganSms(...)) kita — hanya domainnya berbeda. Satu kerabat dekat lainnya adalah Collections.unmodifiableList, pembungkus yang menolak seluruh operasi perubahan tanpa menyalin data.

Notifikasi kita kini bisa tumbuh perilaku tanpa menambah class kombinasi. Namun seluruh pola sejauh ini masih bekerja pada objek di memori — belum ada pertanyaan “bagaimana objek domain bertemu media simpan”. Chapter berikutnya memperkenalkan pintunya: repository pattern, koleksi domain yang menyerap detail persistensi.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

NotifikasiBerlapis.java

Unduh berkas
java
public class NotifikasiBerlapis {
    public static void main(String[] args) {
        // Komponen dasar: hanya email
        Notifikasi dasar = new NotifikasiEmail();
        System.out.println("== email saja ==");
        dasar.kirim("Pesanan sudah dikirim");
        System.out.println("biaya per kirim: Rp" + dasar.biaya());

        // Bungkus dengan sms: perilaku bertambah tanpa mengubah class dasar
        Notifikasi denganSms = new DenganSms(dasar);
        System.out.println("== email + sms ==");
        denganSms.kirim("Pesanan sudah dikirim");
        System.out.println("biaya per kirim: Rp" + denganSms.biaya());

        // Bungkus lagi dengan log: dekorator boleh bertumpuk
        Notifikasi lengkap = new DenganLog(new DenganSms(dasar));
        System.out.println("== email + sms + log ==");
        lengkap.kirim("Pesanan sudah dikirim");
        System.out.println("biaya per kirim: Rp" + lengkap.biaya());
    }
}

// Komponen: antarmuka yang sama untuk objek dasar maupun pembungkusnya
interface Notifikasi {
    void kirim(String pesan);

    long biaya(); // biaya per kiriman dalam rupiah
}

class NotifikasiEmail implements Notifikasi {
    @Override
    public void kirim(String pesan) {
        System.out.println("email : mengirim \"" + pesan + "\"");
    }

    @Override
    public long biaya() {
        return 100;
    }
}

// Dekorator dasar: memegang komponen yang dibungkus dan meneruskan seluruh method
abstract class DekoratorNotifikasi implements Notifikasi {
    protected final Notifikasi dibungkus;

    DekoratorNotifikasi(Notifikasi dibungkus) {
        this.dibungkus = dibungkus;
    }
}

class DenganSms extends DekoratorNotifikasi {
    DenganSms(Notifikasi dibungkus) {
        super(dibungkus);
    }

    @Override
    public void kirim(String pesan) {
        dibungkus.kirim(pesan);
        System.out.println("sms   : mengirim \"" + pesan + "\"");
    }

    @Override
    public long biaya() {
        return dibungkus.biaya() + 500; // biaya berjenjang: sms menambah 500
    }
}

class DenganLog extends DekoratorNotifikasi {
    DenganLog(Notifikasi dibungkus) {
        super(dibungkus);
    }

    @Override
    public void kirim(String pesan) {
        System.out.println("log   : mulai pengiriman \"" + pesan + "\"");
        dibungkus.kirim(pesan);
        System.out.println("log   : pengiriman selesai \"" + pesan + "\"");
    }

    @Override
    public long biaya() {
        return dibungkus.biaya() + 10; // pencatatan murah: hanya 10
    }
}