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.MultiWriter adalah contoh Composite Pattern di standard library — ia menggabungkan beberapa io.Writer dan mendelegasikan setiap Write() ke semua writer yang terdaftar. http.Handler juga 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 utamaGetSize(), Search(), Print(), Count() semuanya berjalan otomatis ke seluruh sub-tree; client cukup memanggil sekali di root.
  • Composite method hanya di Composite, bukan di interfaceAdd(), 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.
  • Apply dan Collect adalah fungsi helper yang sangat berguna untuk operasi bulk pada seluruh tree tanpa mengubah Component interface.
  • Contoh di standard library: io.MultiWriter dan 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.

← Sebelumnya: Bridge   Berikutnya: Decorator →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact