Composite Pattern #
Saat membangun fitur perhitungan harga di sistem e-commerce, kamu mungkin menemukan skenario ini: sebuah “produk” bisa berupa item tunggal seharga Rp 50.000, atau bisa berupa bundle yang berisi beberapa item, atau bahkan bundle yang berisi bundle lain. Kode yang menghitung total harga perlu berjalan dengan cara yang sama untuk ketiganya — tanpa peduli apakah ia sedang memproses satu item atau seluruh hierarki bundle bersarang. Tanpa Composite Pattern, kode itu akan penuh dengan pengecekan tipe: if isBundle { for each item ... } else { return price }. Setiap kali variasi baru ditambahkan, seluruh blok kondisional harus diperbarui. Composite Pattern menghilangkan masalah ini dengan memastikan bahwa objek tunggal dan kumpulan objek mengimplementasikan interface yang sama — sehingga kode client bisa memanggil CalculatePrice() pada apapun tanpa perlu tahu apakah itu leaf atau composite.
Apa itu Composite Pattern? #
Composite Pattern adalah structural design pattern yang menyusun objek ke dalam struktur tree untuk merepresentasikan relasi part-whole, dan memungkinkan client memperlakukan objek individual maupun komposisi objek secara seragam melalui satu interface yang sama.
Dua konsep yang bekerja bersama:
- Keseragaman — leaf (daun) dan composite (cabang) mengimplementasikan interface yang sama; client tidak perlu membedakan keduanya
- Rekursivitas — composite menyimpan daftar children yang juga bertipe interface yang sama; operasi pada composite secara otomatis menyebar ke seluruh sub-tree
Di Golang, Composite Pattern sangat natural karena interface memungkinkan keseragaman tanpa inheritance. Satu interface, dua jenis implementasi — leaf yang tidak punya children, dan composite yang mendelegasikan operasi ke children-nya.
flowchart TD
subgraph "Struktur Tree Composite"
Root["📁 Root\n(Composite)"]
Root --> A["📁 Documents\n(Composite)"]
Root --> B["📁 Images\n(Composite)"]
Root --> C["📄 readme.txt\n(Leaf)"]
A --> D["📄 invoice.pdf\n(Leaf)"]
A --> E["📄 contract.docx\n(Leaf)"]
B --> F["📁 2024\n(Composite)"]
B --> G["📄 logo.png\n(Leaf)"]
F --> H["📄 jan.jpg\n(Leaf)"]
F --> I["📄 feb.jpg\n(Leaf)"]
end
Client -->|"GetSize()\nsame call for all"| Root
Root -->|"GetSize()\nrekursif"| A & B & C
A -->|"GetSize()"| D & E
Masalah yang Dipecahkan #
Tanpa Composite Pattern, kode yang memproses struktur hierarkis biasanya berakhir dengan dua masalah besar yang saling berkaitan.
Masalah 1: Pengecekan Tipe yang Tersebar #
// ANTI-PATTERN: client harus tahu tipe konkret setiap node
type File struct {
Name string
Size int64
}
type Folder struct {
Name string
Files []*File // hanya bisa berisi File — tidak bisa nested folder
Subfolders []*Folder
}
// Client harus menangani File dan Folder secara berbeda
func calculateTotalSize(folder *Folder) int64 {
var total int64
for _, file := range folder.Files {
total += file.Size // handle File
}
for _, sub := range folder.Subfolders {
total += calculateTotalSize(sub) // handle Folder secara berbeda
}
return total
}
// Masalah: jika ada tipe baru (misalnya Symlink), semua fungsi harus diubah
Masalah 2: Duplikasi Logika untuk Setiap Tipe #
Fungsi calculateTotalSize, printTree, search, compress — semuanya harus punya penanganan terpisah untuk File dan Folder. Setiap kali tipe baru ditambahkan, semua fungsi itu harus diperbarui. Ini melanggar Open-Closed Principle secara masif.
// BENAR: satu interface, client tidak perlu tahu tipenya
type FileSystemNode interface {
GetName() string
GetSize() int64
Print(indent int)
}
// File (Leaf) dan Folder (Composite) keduanya FileSystemNode
func calculateTotalSize(node FileSystemNode) int64 {
return node.GetSize() // sama untuk semua tipe — polimorfisme rekursif
}
Tiga Komponen Utama #
Composite Pattern terdiri dari tiga peran yang harus dipahami sebelum implementasi.
classDiagram
class FileSystemNode {
<<interface>>
+GetName() string
+GetSize() int64
+GetPath() string
+Print(indent int)
+Search(query string) []FileSystemNode
}
class File {
-name string
-size int64
-path string
-mimeType string
+GetName() string
+GetSize() int64
+GetPath() string
+Print(indent int)
+Search(query string) []FileSystemNode
}
class Folder {
-name string
-path string
-children []FileSystemNode
+GetName() string
+GetSize() int64
+GetPath() string
+Print(indent int)
+Search(query string) []FileSystemNode
+Add(node FileSystemNode)
+Remove(name string) bool
+GetChildren() []FileSystemNode
}
FileSystemNode <|.. File : Leaf
FileSystemNode <|.. Folder : Composite
Folder o-- FileSystemNode : children
| Komponen | Peran | Implementasi |
|---|---|---|
| Component | Interface yang diimplementasikan semua node | FileSystemNode |
| Leaf | Node tanpa children — unit terkecil | File, MenuItem, Employee |
| Composite | Node dengan children — mendelegasikan ke children | Folder, MenuGroup, Department |
Implementasi Lengkap: File System #
File system adalah contoh paling klasik dan paling intuitif untuk Composite Pattern — setiap orang sudah familiar dengan konsep file dan folder yang bisa bersarang.
Component Interface #
package filesystem
import "time"
// FileSystemNode adalah Component interface — kontrak untuk semua node dalam tree.
// Baik File maupun Folder mengimplementasikan interface ini.
type FileSystemNode interface {
GetName() string
GetSize() int64 // untuk File: ukuran file; untuk Folder: total ukuran semua isi
GetPath() string
GetModifiedAt() time.Time
Print(indent int) // tampilkan struktur dengan indentasi
Search(query string) []FileSystemNode // cari node berdasarkan nama
Count() int // jumlah node: File=1, Folder=jumlah semua isinya rekursif
}
Leaf: File #
package filesystem
import (
"fmt"
"strings"
"time"
)
// File adalah Leaf — tidak punya children, unit terkecil dalam tree.
type File struct {
name string
size int64
path string
mimeType string
modifiedAt time.Time
}
func NewFile(name, path, mimeType string, size int64) *File {
return &File{
name: name,
size: size,
path: path,
mimeType: mimeType,
modifiedAt: time.Now(),
}
}
func (f *File) GetName() string { return f.name }
func (f *File) GetSize() int64 { return f.size }
func (f *File) GetPath() string { return f.path }
func (f *File) GetModifiedAt() time.Time { return f.modifiedAt }
func (f *File) GetMIMEType() string { return f.mimeType }
// Print menampilkan file dengan indentasi yang sesuai.
func (f *File) Print(indent int) {
fmt.Printf("%s📄 %s (%s, %s)\n",
strings.Repeat(" ", indent),
f.name,
f.mimeType,
humanizeSize(f.size),
)
}
// Search mengembalikan dirinya sendiri jika nama cocok, slice kosong jika tidak.
func (f *File) Search(query string) []FileSystemNode {
if strings.Contains(strings.ToLower(f.name), strings.ToLower(query)) {
return []FileSystemNode{f}
}
return nil
}
// Count selalu mengembalikan 1 — File adalah satu node.
func (f *File) Count() int { return 1 }
// humanizeSize mengonversi bytes ke format yang mudah dibaca.
func humanizeSize(bytes int64) string {
switch {
case bytes >= 1<<30:
return fmt.Sprintf("%.1f GB", float64(bytes)/(1<<30))
case bytes >= 1<<20:
return fmt.Sprintf("%.1f MB", float64(bytes)/(1<<20))
case bytes >= 1<<10:
return fmt.Sprintf("%.1f KB", float64(bytes)/(1<<10))
default:
return fmt.Sprintf("%d B", bytes)
}
}
Composite: Folder #
package filesystem
import (
"fmt"
"strings"
"time"
)
// Folder adalah Composite — berisi children yang juga FileSystemNode.
// Semua operasi didelegasikan secara rekursif ke children.
type Folder struct {
name string
path string
children []FileSystemNode
modifiedAt time.Time
}
func NewFolder(name, path string) *Folder {
return &Folder{
name: name,
path: path,
children: make([]FileSystemNode, 0),
modifiedAt: time.Now(),
}
}
func (f *Folder) GetName() string { return f.name }
func (f *Folder) GetPath() string { return f.path }
func (f *Folder) GetModifiedAt() time.Time { return f.modifiedAt }
// GetSize mengembalikan total ukuran semua isi folder secara rekursif.
// Client tidak perlu tahu bahwa ini melibatkan rekursi — cukup panggil GetSize().
func (f *Folder) GetSize() int64 {
var total int64
for _, child := range f.children {
total += child.GetSize() // rekursi otomatis — setiap child tahu cara menghitung ukurannya
}
return total
}
// Print menampilkan folder dan seluruh isinya secara rekursif.
func (f *Folder) Print(indent int) {
fmt.Printf("%s📁 %s/ (%s, %d items)\n",
strings.Repeat(" ", indent),
f.name,
humanizeSize(f.GetSize()),
len(f.children),
)
for _, child := range f.children {
child.Print(indent + 1) // setiap child tahu cara menampilkan dirinya sendiri
}
}
// Search mencari node berdasarkan nama di seluruh sub-tree secara rekursif.
func (f *Folder) Search(query string) []FileSystemNode {
var results []FileSystemNode
// Cek apakah folder itu sendiri cocok
if strings.Contains(strings.ToLower(f.name), strings.ToLower(query)) {
results = append(results, f)
}
// Delegasikan pencarian ke setiap child
for _, child := range f.children {
results = append(results, child.Search(query)...)
}
return results
}
// Count mengembalikan jumlah total semua node dalam sub-tree (termasuk dirinya sendiri).
func (f *Folder) Count() int {
total := 1 // folder itu sendiri
for _, child := range f.children {
total += child.Count()
}
return total
}
// Add menambahkan node ke folder ini.
func (f *Folder) Add(node FileSystemNode) {
f.children = append(f.children, node)
f.modifiedAt = time.Now()
}
// Remove menghapus node berdasarkan nama. Mengembalikan true jika berhasil dihapus.
func (f *Folder) Remove(name string) bool {
for i, child := range f.children {
if child.GetName() == name {
f.children = append(f.children[:i], f.children[i+1:]...)
f.modifiedAt = time.Now()
return true
}
}
return false
}
// GetChildren mengembalikan salinan daftar children.
func (f *Folder) GetChildren() []FileSystemNode {
result := make([]FileSystemNode, len(f.children))
copy(result, f.children)
return result
}
// FindFolder mencari subfolder berdasarkan nama (hanya satu level).
func (f *Folder) FindFolder(name string) *Folder {
for _, child := range f.children {
if folder, ok := child.(*Folder); ok && folder.GetName() == name {
return folder
}
}
return nil
}
Penggunaan: Client Tidak Peduli Tipe #
func main() {
// Bangun struktur file system
root := filesystem.NewFolder("root", "/")
documents := filesystem.NewFolder("documents", "/documents")
documents.Add(filesystem.NewFile("invoice_2024.pdf", "/documents/invoice_2024.pdf", "application/pdf", 245760))
documents.Add(filesystem.NewFile("contract.docx", "/documents/contract.docx", "application/docx", 102400))
reports := filesystem.NewFolder("reports", "/documents/reports")
reports.Add(filesystem.NewFile("q1_2024.xlsx", "/documents/reports/q1_2024.xlsx", "application/xlsx", 512000))
reports.Add(filesystem.NewFile("q2_2024.xlsx", "/documents/reports/q2_2024.xlsx", "application/xlsx", 487424))
documents.Add(reports)
images := filesystem.NewFolder("images", "/images")
images.Add(filesystem.NewFile("logo.png", "/images/logo.png", "image/png", 51200))
images.Add(filesystem.NewFile("banner.jpg", "/images/banner.jpg", "image/jpeg", 204800))
root.Add(documents)
root.Add(images)
root.Add(filesystem.NewFile("readme.txt", "/readme.txt", "text/plain", 1024))
// Client memanggil operasi yang sama pada semua node — tidak peduli Leaf atau Composite
fmt.Println("=== File System Structure ===")
root.Print(0)
fmt.Printf("\nTotal size: %s\n", humanizeSize(root.GetSize()))
fmt.Printf("Total nodes: %d\n", root.Count())
fmt.Println("\n=== Search for 'q' ===")
results := root.Search("q")
for _, node := range results {
fmt.Printf("Found: %s (%s)\n", node.GetName(), node.GetPath())
}
// Operasi yang sama bisa dipanggil pada sub-folder
fmt.Printf("\nDocuments size: %s\n", humanizeSize(documents.GetSize()))
fmt.Printf("Reports size: %s\n", humanizeSize(reports.GetSize()))
}
Output yang dihasilkan:
=== File System Structure ===
📁 root/ (1.5 MB, 3 items)
📁 documents/ (1.3 MB, 3 items)
📄 invoice_2024.pdf (application/pdf, 240.0 KB)
📄 contract.docx (application/docx, 100.0 KB)
📁 reports/ (976.0 KB, 2 items)
📄 q1_2024.xlsx (application/xlsx, 500.0 KB)
📄 q2_2024.xlsx (application/xlsx, 476.0 KB)
📁 images/ (249.0 KB, 2 items)
📄 logo.png (image/png, 50.0 KB)
📄 banner.jpg (image/jpeg, 200.0 KB)
📄 readme.txt (text/plain, 1.0 KB)
Total size: 1.5 MB
Total nodes: 8
Studi Kasus Kedua: Organization Chart #
Organization chart adalah use case Composite Pattern yang sangat relevan di aplikasi enterprise — setiap karyawan bisa menjadi manager yang punya laporan, dan setiap manager bisa melapor ke manager lain.
package org
import (
"fmt"
"strings"
)
// Employee adalah Component interface untuk semua node dalam org chart.
type Employee interface {
GetName() string
GetTitle() string
GetSalary() float64 // total salary: untuk individu=gajinya, untuk manager=gaji+bawahan
GetHeadcount() int // jumlah total orang di bawah (termasuk dirinya)
Print(indent int)
GetDepartment() string
}
// IndividualContributor adalah Leaf — tidak punya direct report.
type IndividualContributor struct {
name string
title string
salary float64
department string
}
func NewIC(name, title, department string, salary float64) *IndividualContributor {
return &IndividualContributor{name: name, title: title, salary: salary, department: department}
}
func (e *IndividualContributor) GetName() string { return e.name }
func (e *IndividualContributor) GetTitle() string { return e.title }
func (e *IndividualContributor) GetSalary() float64 { return e.salary }
func (e *IndividualContributor) GetHeadcount() int { return 1 }
func (e *IndividualContributor) GetDepartment() string { return e.department }
func (e *IndividualContributor) Print(indent int) {
fmt.Printf("%s👤 %s (%s) — Rp %.0f\n",
strings.Repeat(" ", indent), e.name, e.title, e.salary)
}
// Manager adalah Composite — punya direct reports yang juga Employee.
type Manager struct {
name string
title string
baseSalary float64
department string
directReports []Employee
}
func NewManager(name, title, department string, baseSalary float64) *Manager {
return &Manager{
name: name,
title: title,
baseSalary: baseSalary,
department: department,
directReports: make([]Employee, 0),
}
}
func (m *Manager) AddReport(e Employee) {
m.directReports = append(m.directReports, e)
}
func (m *Manager) GetName() string { return m.name }
func (m *Manager) GetTitle() string { return m.title }
func (m *Manager) GetDepartment() string { return m.department }
// GetSalary mengembalikan total salary seluruh tim — rekursif ke bawah.
func (m *Manager) GetSalary() float64 {
total := m.baseSalary
for _, report := range m.directReports {
total += report.GetSalary()
}
return total
}
// GetHeadcount mengembalikan jumlah total orang di bawah manager ini (termasuk dirinya).
func (m *Manager) GetHeadcount() int {
total := 1 // dirinya sendiri
for _, report := range m.directReports {
total += report.GetHeadcount()
}
return total
}
func (m *Manager) Print(indent int) {
fmt.Printf("%s👔 %s (%s) — Rp %.0f [total tim: Rp %.0f, %d orang]\n",
strings.Repeat(" ", indent),
m.name, m.title,
m.baseSalary,
m.GetSalary(),
m.GetHeadcount(),
)
for _, report := range m.directReports {
report.Print(indent + 1)
}
}
Penggunaan:
func main() {
// Bangun org chart
cto := org.NewManager("Budi", "CTO", "Engineering", 50_000_000)
backendLead := org.NewManager("Citra", "Backend Lead", "Engineering", 30_000_000)
backendLead.AddReport(org.NewIC("Dani", "Senior Engineer", "Engineering", 20_000_000))
backendLead.AddReport(org.NewIC("Eka", "Mid Engineer", "Engineering", 15_000_000))
backendLead.AddReport(org.NewIC("Fajar", "Junior Engineer", "Engineering", 10_000_000))
frontendLead := org.NewManager("Gita", "Frontend Lead", "Engineering", 28_000_000)
frontendLead.AddReport(org.NewIC("Hadi", "Senior Engineer", "Engineering", 20_000_000))
frontendLead.AddReport(org.NewIC("Indah", "Mid Engineer", "Engineering", 15_000_000))
cto.AddReport(backendLead)
cto.AddReport(frontendLead)
cto.AddReport(org.NewIC("Joko", "DevOps Engineer", "Engineering", 22_000_000))
// Panggil operasi yang sama pada semua level — client tidak peduli IC atau Manager
fmt.Println("=== Organization Chart ===")
cto.Print(0)
fmt.Printf("\nTotal engineering headcount: %d orang\n", cto.GetHeadcount())
fmt.Printf("Total engineering salary budget: Rp %.0f\n", cto.GetSalary())
fmt.Printf("Backend team salary budget: Rp %.0f\n", backendLead.GetSalary())
}
Composite dengan Operasi yang Lebih Kompleks #
Composite Pattern juga mendukung operasi yang memerlukan state saat traversal, bukan hanya agregasi sederhana.
// Operasi Apply — menjalankan fungsi pada setiap node dalam tree.
// Berguna untuk bulk operation: compress semua file, update permission, dll.
func Apply(node FileSystemNode, fn func(FileSystemNode)) {
fn(node)
if folder, ok := node.(*Folder); ok {
for _, child := range folder.GetChildren() {
Apply(child, fn) // rekursi ke seluruh sub-tree
}
}
}
// Collect — mengumpulkan semua node yang memenuhi predicate.
func Collect(node FileSystemNode, predicate func(FileSystemNode) bool) []FileSystemNode {
var results []FileSystemNode
Apply(node, func(n FileSystemNode) {
if predicate(n) {
results = append(results, n)
}
})
return results
}
// Penggunaan: temukan semua PDF yang lebih besar dari 1MB
largePDFs := Collect(root, func(n FileSystemNode) bool {
file, ok := n.(*File)
return ok && file.GetMIMEType() == "application/pdf" && file.GetSize() > 1<<20
})
// Penggunaan: log semua node saat traversal
Apply(root, func(n FileSystemNode) {
log.Printf("Processing: %s", n.GetPath())
})
sequenceDiagram
participant C as Client
participant R as Root Folder
participant D as Documents Folder
participant F1 as invoice.pdf
participant R2 as Reports Folder
participant F2 as q1.xlsx
C->>R: GetSize()
R->>D: GetSize()
D->>F1: GetSize() → 245760
D->>R2: GetSize()
R2->>F2: GetSize() → 512000
R2-->>D: 512000
D-->>R: 245760 + 102400 + 512000 + 487424
R-->>C: total semua isi
Note over C,F2: Client hanya panggil sekali — rekursi terjadi di balik layar
Mencegah Cyclic Reference #
Salah satu risiko terbesar Composite Pattern adalah cyclic reference — folder A berisi folder B yang berisi folder A lagi. Ini menyebabkan infinite recursion dan stack overflow.
// ANTI-PATTERN: tidak ada perlindungan dari cyclic reference
func (f *Folder) Add(node FileSystemNode) {
f.children = append(f.children, node)
// jika node adalah parent dari f, terjadi cycle — GetSize() akan infinite loop
}
// BENAR: validasi sebelum menambahkan node
func (f *Folder) Add(node FileSystemNode) error {
// Cek apakah node adalah ancestor dari folder ini
if f.isAncestorOf(node) {
return fmt.Errorf("cannot add %q to %q: would create cyclic reference", node.GetName(), f.name)
}
f.children = append(f.children, node)
f.modifiedAt = time.Now()
return nil
}
// isAncestorOf memeriksa apakah target adalah ancestor dari folder ini.
func (f *Folder) isAncestorOf(target FileSystemNode) bool {
if f == target {
return true
}
for _, child := range f.children {
if childFolder, ok := child.(*Folder); ok {
if childFolder.isAncestorOf(target) {
return true
}
}
}
return false
}
Cyclic Reference Menyebabkan Stack Overflow
Jika composite tree memiliki cyclic reference, setiap operasi rekursif —
GetSize(),Print(),Search()— akan berjalan tanpa henti sampai goroutine kehabisan stack dan panic. Selalu validasi bahwa tidak ada cycle sebelum menambahkan node ke tree, terutama jika node bisa ditambahkan secara dinamis dari input pengguna.
Testing Composite Pattern #
Testing Composite Pattern berfokus pada dua aspek: operasi pada Leaf tunggal bekerja benar, dan operasi pada Composite mendelegasikan ke seluruh children dengan benar.
func TestFile_GetSize(t *testing.T) {
file := filesystem.NewFile("test.pdf", "/test.pdf", "application/pdf", 102400)
if file.GetSize() != 102400 {
t.Errorf("expected size 102400, got %d", file.GetSize())
}
if file.Count() != 1 {
t.Errorf("expected count 1 for leaf, got %d", file.Count())
}
}
func TestFolder_GetSize_AggregatesChildren(t *testing.T) {
folder := filesystem.NewFolder("test", "/test")
folder.Add(filesystem.NewFile("a.txt", "/test/a.txt", "text/plain", 1000))
folder.Add(filesystem.NewFile("b.txt", "/test/b.txt", "text/plain", 2000))
subFolder := filesystem.NewFolder("sub", "/test/sub")
subFolder.Add(filesystem.NewFile("c.txt", "/test/sub/c.txt", "text/plain", 3000))
folder.Add(subFolder)
expectedSize := int64(1000 + 2000 + 3000)
if folder.GetSize() != expectedSize {
t.Errorf("expected total size %d, got %d", expectedSize, folder.GetSize())
}
if folder.Count() != 4 { // folder itu sendiri + 3 file
t.Errorf("expected count 4, got %d", folder.Count())
}
}
func TestFolder_Search_FindsInSubTree(t *testing.T) {
root := filesystem.NewFolder("root", "/")
sub := filesystem.NewFolder("documents", "/documents")
sub.Add(filesystem.NewFile("invoice.pdf", "/documents/invoice.pdf", "application/pdf", 1000))
root.Add(sub)
root.Add(filesystem.NewFile("readme.txt", "/readme.txt", "text/plain", 500))
// Cari "invoice" dari root — harus menembus ke dalam subfolder
results := root.Search("invoice")
if len(results) != 1 {
t.Errorf("expected 1 result, got %d", len(results))
}
if results[0].GetName() != "invoice.pdf" {
t.Errorf("expected 'invoice.pdf', got %q", results[0].GetName())
}
}
func TestFolder_Remove(t *testing.T) {
folder := filesystem.NewFolder("test", "/test")
folder.Add(filesystem.NewFile("a.txt", "/test/a.txt", "text/plain", 1000))
folder.Add(filesystem.NewFile("b.txt", "/test/b.txt", "text/plain", 2000))
removed := folder.Remove("a.txt")
if !removed {
t.Error("expected Remove to return true")
}
if folder.Count() != 2 { // folder + b.txt
t.Errorf("expected count 2 after remove, got %d", folder.Count())
}
if folder.Remove("nonexistent.txt") {
t.Error("expected Remove to return false for nonexistent file")
}
}
func TestUniformTreatment(t *testing.T) {
// Test bahwa operasi yang sama bekerja untuk Leaf dan Composite
nodes := []filesystem.FileSystemNode{
filesystem.NewFile("file.txt", "/file.txt", "text/plain", 1000),
func() filesystem.FileSystemNode {
f := filesystem.NewFolder("folder", "/folder")
f.Add(filesystem.NewFile("child.txt", "/folder/child.txt", "text/plain", 500))
return f
}(),
}
for _, node := range nodes {
// Operasi ini harus bekerja untuk semua tipe tanpa type assertion
_ = node.GetName()
_ = node.GetSize()
_ = node.GetPath()
_ = node.Count()
_ = node.Search("test")
// Tidak ada if/else berdasarkan tipe — inilah keseragaman Composite Pattern
}
}
Kapan Menggunakan dan Kapan Tidak #
GUNAKAN Composite jika:
✓ Data memiliki struktur hierarkis (tree) yang alami
✓ Client perlu memperlakukan leaf dan composite dengan cara yang sama
✓ Operasi bersifat rekursif — berlaku untuk satu node dan seluruh sub-tree-nya
✓ Ingin menambah tipe node baru tanpa mengubah kode client
✓ Ingin menghilangkan if/else berbasis tipe di kode yang memproses tree
HINDARI Composite jika:
✗ Struktur tidak hierarkis — graph, flat list, atau relasi many-to-many
✗ Leaf dan Composite memiliki operasi yang sangat berbeda — interface menjadi terlalu lebar
✗ Tree sangat dalam dan operasi rekursif bisa menyebabkan stack overflow
✗ Perlu akses ke parent node dari child — Composite standar tidak menyimpan referensi ke parent
Composite di Standard Library Golang
io.MultiWriteradalah contoh Composite Pattern di standard library — ia menggabungkan beberapaio.Writerdan mendelegasikan setiapWrite()ke semua writer yang terdaftar.http.Handlerjuga sering dikombinasikan secara composite melalui middleware chain. Memahami Composite Pattern membantu kamu membaca dan memahami pola-pola ini dengan lebih cepat.
Checklist Review Composite #
DESAIN:
□ Component interface mendefinisikan operasi yang relevan untuk SEMUA node (leaf dan composite)
□ Composite method (Add, Remove, GetChildren) ada di Composite, bukan di interface
□ Operasi di interface bersifat rekursif — bermakna untuk leaf dan composite
□ Tidak ada type assertion di kode client
IMPLEMENTASI:
□ Leaf mengimplementasikan semua method interface dengan nilai yang tepat (bukan panic)
□ Composite mendelegasikan ke seluruh children, bukan mengimplementasikan sendiri
□ Ada perlindungan dari cyclic reference di method Add()
□ GetChildren() mengembalikan salinan, bukan reference langsung ke internal slice
REKURSI:
□ Setiap operasi rekursif memiliki base case yang jelas (Leaf = terminasi)
□ Tidak ada kemungkinan infinite recursion (tidak ada cycle di tree)
□ Operasi berat (GetSize pada tree besar) dipertimbangkan untuk di-cache
TESTING:
□ Test operasi pada Leaf tunggal
□ Test operasi pada Composite dengan children
□ Test operasi rekursif pada multi-level tree
□ Test cyclic reference detection (jika diimplementasikan)
□ Test bahwa interface dipanggil secara seragam tanpa type assertion
Ringkasan #
- Composite memungkinkan keseragaman — Leaf dan Composite mengimplementasikan interface yang sama, sehingga client bisa memanggil operasi yang sama tanpa perlu tahu tipenya.
- Tiga komponen utama: Component interface, Leaf (unit terkecil tanpa children), dan Composite (mendelegasikan operasi ke children secara rekursif).
- Operasi rekursif adalah kekuatan utama —
GetSize(),Search(),Print(),Count()semuanya berjalan otomatis ke seluruh sub-tree; client cukup memanggil sekali di root.- Composite method hanya di Composite, bukan di interface —
Add(),Remove(),GetChildren()tidak ada di Component interface karena Leaf tidak perlu (dan tidak bisa) mengimplementasikannya.- Wajib melindungi dari cyclic reference — validasi di
Add()sebelum menerima node; cyclic reference menyebabkan stack overflow yang tidak bisa di-recover.ApplydanCollectadalah fungsi helper yang sangat berguna untuk operasi bulk pada seluruh tree tanpa mengubah Component interface.- Contoh di standard library:
io.MultiWriterdan middleware chain HTTP menggunakan prinsip Composite — mendelegasikan operasi ke kumpulan implementasi di balik interface yang sama.- Jangan gunakan untuk struktur non-hierarkis — jika data berbentuk graph atau flat list, Composite Pattern adalah over-engineering yang membuat kode lebih sulit dipahami.