Perilaku baru sering kali paling enak ditambahkan dari luar: kirim ulang email yang gagal, catat setiap pengiriman, kompresi data sebelum ditulis ke jaringan. Menjejalkan semua tugas itu ke dalam implementasi inti membuatnya membengkak dan sulit diuji, apalagi bila implementasinya milik pihak ketiga. Decorator pattern menambahkan perilaku dengan cara membungkus objek di dalam objek lain yang mengimplementasi interface yang sama — pemanggil tidak melihat perbedaan apa pun. Go tidak punya inheritance, sehingga pembungkusan bukan cuma jalan idiomatik, melainkan satu-satunya jalan; standard library sendiri memakainya di mana-mana.

Membungkus Tanpa Mengubah #

Dekorator menyimpan satu nilai yang dibungkus — biasanya di field bernama berikut — lalu mengimplementasi interface yang sama dengan nilai tersebut. Method dekorator mengerjakan bagiannya, kemudian mendelegasikan sisa pekerjaan ke berikut. Karena lapisan luar hanya mengenal lapisan berikutnya sebagai interface, lapisan apa pun boleh ditukar tanpa mengubah yang lain.

go
// Susunan tiga lapis: Log membungkus Retry, Retry membungkus Email.
notifier := &LogNotifier{
	berikut: &RetryNotifier{
		berikut: &emailFlaky{gagalTersisa: 2},
		maks:    3,
	},
}

Pemanggil notifier.Send(...) hanya melihat satu Notifier. Susunan bertingkat seperti ini bekerja seperti bawang: permintaan menembus lapisan terluar sampai ke inti, lalu hasilnya mengalir kembali keluar melewati tiap lapisan yang sama.

Notifier dengan Log dan Retry #

Dua dekorator pada demo memiliki tugas yang berbeda. LogNotifier mencetak baris log sebelum mendelegasikan Send dan sesudah hasilnya kembali — perilaku pembukti. RetryNotifier mengulang panggilan sampai maks kali ketika lapisan berikutnya mengembalikan error, lalu membungkus error terakhir dengan jumlah percobaan.

Objek inti emailFlaky sengaja dibuat tidak stabil dengan cara yang terkendali: field gagalTersisa menentukan dua kegagalan pertama sebelum pengiriman berhasil. Pola tiruan semacam ini berguna ganda — untuk demo yang deterministik dan sebagai test double saat pengujian dekorator di isolasi.

io.Reader: Dekorator Asli Standard Library #

Interface io.Reader adalah contoh dekorator paling terkenal di Go. Konstruktor seperti gzip.NewReader menerima pembaca apa pun, membungkusnya, lalu mengembalikan pembaca baru yang isinya sama namun tingkahannya berbeda — mendekompresi. bufio.NewReader melakukan hal serupa dengan menambahkan buffering. Rantai berikut membalikkan pembacaan data terkompresi dari memori:

go
// Rantai dekorator standard library: bytes.Reader -> gzip.Reader.
zr, err := gzip.NewReader(bytes.NewReader(kompres))

Tidak ada interface baru dan tidak ada perubahan pada bytes.Reader; pembungkusan murni terjadi di luar. Inilah bukti bahwa pola ini bukan teori — pembacaan berkas gzip di Go selalu berbentuk tumpukan dekorator os.File → gzip.Reader → bufio.Reader.

Demo Lengkap #

Berkas decorator-demo.go pada bagian bawah halaman menjalankan kedua bagian di atas: tiga lapis Notifier yang mengirim pesanan yang gagal dua kali, lalu kompresi dan dekompresi teks lewat rantai gzip. Kode lengkapnya adalah sebagai berikut:

go
package main

import (
	"bytes"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"strings"
)

// Notifier adalah interface yang dipertahankan oleh dekorator.
// Dekorator membungkus interface yang sama sehingga bisa bertumpuk.
type Notifier interface {
	Send(msg string) error
}

// emailFlaky adalah notifikasi tiruan yang gagal dua kali pertama
// untuk mensimulasikan jaringan tidak stabil.
type emailFlaky struct {
	gagalTersisa int
}

func (n *emailFlaky) Send(msg string) error {
	if n.gagalTersisa > 0 {
		n.gagalTersisa--
		return errors.New("jaringan sibuk")
	}
	fmt.Println("terkirim :", msg)
	return nil
}

// RetryNotifier adalah dekorator: mengulang pengiriman sampai maks kali.
type RetryNotifier struct {
	berikut Notifier
	maks    int
}

func (r *RetryNotifier) Send(msg string) error {
	var err error
	for i := 1; i <= r.maks; i++ {
		if err = r.berikut.Send(msg); err == nil {
			return nil
		}
		fmt.Printf("coba %d    : gagal (%v)\n", i, err)
	}
	return fmt.Errorf("gagal setelah %d percobaan: %w", r.maks, err)
}

// LogNotifier adalah dekorator kedua: mencatat sebelum dan sesudah.
type LogNotifier struct {
	berikut Notifier
}

func (l *LogNotifier) Send(msg string) error {
	fmt.Printf("log       : mengirim %q\n", msg)
	err := l.berikut.Send(msg)
	if err != nil {
		fmt.Printf("log       : gagal (%v)\n", err)
		return err
	}
	fmt.Printf("log       : %q terkirim\n", msg)
	return nil
}

func main() {
	// Susunan dekorator: Log(Retry(Email)).
	// emailFlaky gagal dua kali, Retry mengulang, Log mencatat buktinya.
	notifier := &LogNotifier{
		berikut: &RetryNotifier{
			berikut: &emailFlaky{gagalTersisa: 2},
			maks:    3,
		},
	}
	if err := notifier.Send("pesanan #1234 sudah dikirim"); err != nil {
		fmt.Println("error:", err)
	}

	// Dekorator io.Reader di standard library:
	// bytes.Reader -> gzip.Reader -> hasil identik dengan aslinya.
	asli := []byte(strings.Repeat("belajar pola desain di Go. ", 6))
	var buf bytes.Buffer
	zw := gzip.NewWriter(&buf)
	if _, err := zw.Write(asli); err != nil {
		fmt.Println("error:", err)
		return
	}
	zw.Close()
	kompres := buf.Bytes()
	fmt.Printf("asli %d byte -> terkompresi %d byte\n", len(asli), len(kompres))

	zr, err := gzip.NewReader(bytes.NewReader(kompres))
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	hasil, err := io.ReadAll(zr)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("hasil dekompresi identik :", bytes.Equal(hasil, asli))
	fmt.Println("jumlah kata              :", len(strings.Fields(string(hasil))))
}

Output yang Dihasilkan:

text
log       : mengirim "pesanan #1234 sudah dikirim"
coba 1    : gagal (jaringan sibuk)
coba 2    : gagal (jaringan sibuk)
terkirim : pesanan #1234 sudah dikirim
log       : "pesanan #1234 sudah dikirim" terkirim
asli 162 byte -> terkompresi 51 byte
hasil dekompresi identik : true
jumlah kata              : 30

Mari kita telaah baris demi baris program di atas:

  1. Baris 14–16 (type Notifier interface): kontrak yang dipertahankan semua lapisan; dekorator dan inti sama-sama mengimplementasinya.
  2. Baris 20–31 (emailFlaky): objek inti yang menolak dua panggilan pertama dengan error jaringan sibuk sebelum akhirnya berhasil.
  3. Baris 39–47 (RetryNotifier.Send): loop sampai maks kali; error terakhir dibungkus %w agar tetap bisa diperiksa errors.Is di lapisan luar.
  4. Baris 55–64 (LogNotifier.Send): mencetak sebelum dan sesudah mendelegasikan; error dari berikut diteruskan tanpa ditelan.
  5. Baris 69–74 (susunan di main): pembungkusan ditulis sebagai struct literal bersarang; urutan lapisan terlihat jelas dari lekukan.
  6. Baris 83–90 (kompresi): gzip.NewWriter membungkus bytes.Buffer — dekorator versi penulis; zw.Close() wajib dipanggil agar trailer gzip ditulis.
  7. Baris 92–97 (dekompresi): gzip.NewReader membungkus bytes.NewReader — dekorator versi pembaca; io.ReadAll menguras isinya.
  8. Baris 102–103 (verifikasi): bytes.Equal membuktikan hasil dekompresi identik dengan aslinya, dan jumlah kata menegaskan isi tidak rusak.

Dekorator vs Inheritance #

Di bahasa berorientasi objek klasik, dekorator adalah penawar ledakan subclass: satu subclass per kombinasi perilaku diganti susunan bungkus yang bebas. Perbandingannya dirangkum berikut:

AspekDekorator (komposisi)Inheritance (subclass)
Menambah perilakubungkus objek saat runtimedeklarasikan subclass baru
Kombinasi perilakususun lapisan semaunyatetap, dipaku saat kompilasi
Jumlah tipe bantusatu dekorator per perilakusatu subclass per kombinasi
Dukungan Gopenuh — komposisi structtidak ada inheritance

Dekorator menambah perilaku di sekeliling satu objek. Chapter berikutnya memindahkan ide yang sama ke tingkat pipeline utuh: Middleware Pattern merangkai pemrosesan data bertingkat dengan fungsi func(Handler) Handler yang bisa dilipat menjadi satu.

Referensi #

Berkas Sumber

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

decorator-demo.go

Unduh berkas
go
package main

import (
	"bytes"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"strings"
)

// Notifier adalah interface yang dipertahankan oleh dekorator.
// Dekorator membungkus interface yang sama sehingga bisa bertumpuk.
type Notifier interface {
	Send(msg string) error
}

// emailFlaky adalah notifikasi tiruan yang gagal dua kali pertama
// untuk mensimulasikan jaringan tidak stabil.
type emailFlaky struct {
	gagalTersisa int
}

func (n *emailFlaky) Send(msg string) error {
	if n.gagalTersisa > 0 {
		n.gagalTersisa--
		return errors.New("jaringan sibuk")
	}
	fmt.Println("terkirim :", msg)
	return nil
}

// RetryNotifier adalah dekorator: mengulang pengiriman sampai maks kali.
type RetryNotifier struct {
	berikut Notifier
	maks    int
}

func (r *RetryNotifier) Send(msg string) error {
	var err error
	for i := 1; i <= r.maks; i++ {
		if err = r.berikut.Send(msg); err == nil {
			return nil
		}
		fmt.Printf("coba %d    : gagal (%v)\n", i, err)
	}
	return fmt.Errorf("gagal setelah %d percobaan: %w", r.maks, err)
}

// LogNotifier adalah dekorator kedua: mencatat sebelum dan sesudah.
type LogNotifier struct {
	berikut Notifier
}

func (l *LogNotifier) Send(msg string) error {
	fmt.Printf("log       : mengirim %q\n", msg)
	err := l.berikut.Send(msg)
	if err != nil {
		fmt.Printf("log       : gagal (%v)\n", err)
		return err
	}
	fmt.Printf("log       : %q terkirim\n", msg)
	return nil
}

func main() {
	// Susunan dekorator: Log(Retry(Email)).
	// emailFlaky gagal dua kali, Retry mengulang, Log mencatat buktinya.
	notifier := &LogNotifier{
		berikut: &RetryNotifier{
			berikut: &emailFlaky{gagalTersisa: 2},
			maks:    3,
		},
	}
	if err := notifier.Send("pesanan #1234 sudah dikirim"); err != nil {
		fmt.Println("error:", err)
	}

	// Dekorator io.Reader di standard library:
	// bytes.Reader -> gzip.Reader -> hasil identik dengan aslinya.
	asli := []byte(strings.Repeat("belajar pola desain di Go. ", 6))
	var buf bytes.Buffer
	zw := gzip.NewWriter(&buf)
	if _, err := zw.Write(asli); err != nil {
		fmt.Println("error:", err)
		return
	}
	zw.Close()
	kompres := buf.Bytes()
	fmt.Printf("asli %d byte -> terkompresi %d byte\n", len(asli), len(kompres))

	zr, err := gzip.NewReader(bytes.NewReader(kompres))
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	hasil, err := io.ReadAll(zr)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("hasil dekompresi identik :", bytes.Equal(hasil, asli))
	fmt.Println("jumlah kata              :", len(strings.Fields(string(hasil))))
}