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.
// 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:
// 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:
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:
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 : 30Mari kita telaah baris demi baris program di atas:
- Baris 14–16 (
type Notifier interface): kontrak yang dipertahankan semua lapisan; dekorator dan inti sama-sama mengimplementasinya. - Baris 20–31 (
emailFlaky): objek inti yang menolak dua panggilan pertama dengan errorjaringan sibuksebelum akhirnya berhasil. - Baris 39–47 (
RetryNotifier.Send): loop sampaimakskali; error terakhir dibungkus%wagar tetap bisa diperiksaerrors.Isdi lapisan luar. - Baris 55–64 (
LogNotifier.Send): mencetak sebelum dan sesudah mendelegasikan; error dariberikutditeruskan tanpa ditelan. - Baris 69–74 (susunan di
main): pembungkusan ditulis sebagai struct literal bersarang; urutan lapisan terlihat jelas dari lekukan. - Baris 83–90 (kompresi):
gzip.NewWritermembungkusbytes.Buffer— dekorator versi penulis;zw.Close()wajib dipanggil agar trailer gzip ditulis. - Baris 92–97 (dekompresi):
gzip.NewReadermembungkusbytes.NewReader— dekorator versi pembaca;io.ReadAllmenguras isinya. - Baris 102–103 (verifikasi):
bytes.Equalmembuktikan 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:
| Aspek | Dekorator (komposisi) | Inheritance (subclass) |
|---|---|---|
| Menambah perilaku | bungkus objek saat runtime | deklarasikan subclass baru |
| Kombinasi perilaku | susun lapisan semaunya | tetap, dipaku saat kompilasi |
| Jumlah tipe bantu | satu dekorator per perilaku | satu subclass per kombinasi |
| Dukungan Go | penuh — komposisi struct | tidak 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 #
- Dokumentasi package compress/gzip
—
gzip.Readerdangzip.Writersebagai dekoratorio.Reader/io.Writer. - Dokumentasi package bufio — pembungkus buffering di atas stream apa pun.
- Effective Go — komposisi sebagai pengganti inheritance di Go.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
decorator-demo.go
Unduh berkaspackage 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))))
}