Design Patterns dengan Go — Part 8: Composite
Satukan objek-objek ke dalam struktur pohon untuk merepresentasikan hierarki bagian-keseluruhan. Pelajari Composite pattern di Go.
Bayangkan kamu sedang melihat file explorer di komputermu. Kamu memiliki folder bernama Documents. Di dalam folder ini, terdapat beberapa file individu seperti resume.pdf dan photo.png. Kamu juga memiliki folder lain bernama Projects, yang berisi file kode bernama main.go.
Ketika kamu ingin mengetahui ukuran satu file, kamu cukup memeriksa metadata file tersebut. Namun, jika kamu ingin mengetahui ukuran sebuah folder, kamu mengharapkan komputer secara otomatis memindai folder tersebut, menemukan semua file dan subfolder di dalamnya, menghitung ukurannya, dan menjumlahkannya secara rekursif.
Satu hal yang penting: dari sudut pandang pengguna, baik file maupun folder adalah node sistem berkas. Kamu dapat menyalin, menghapus, atau melihat properti dari keduanya. Composite Pattern memungkinkan kita memperlakukan objek individual (File) dan komposisi dari objek-objek tersebut (Folder) dengan cara yang seragam.
Diagram Konseptual
Composite pattern mengatur objek ke dalam struktur pohon (tree) untuk merepresentasikan hierarki part-whole (bagian-keseluruhan).
classDiagram
direction TB
class FileSystemNode {
<<interface>>
+Name() string
+Size() int
+Print(indent string)
}
class File {
-name string
-size int
+Name() string
+Size() int
+Print(indent string)
}
class Directory {
-name string
-components []FileSystemNode
+Name() string
+Size() int
+Print(indent string)
+Add(component FileSystemNode)
}
File ..|> FileSystemNode
Directory ..|> FileSystemNode
Directory o--> FileSystemNode : contains multiple
Pada struktur di atas:
- Component (
FileSystemNode): Interface yang mendefinisikan operasi umum baik untuk elemen sederhana maupun elemen kompleks dalam pohon. - Leaf (
File): Elemen dasar dari pohon yang tidak memiliki anak. Elemen ini mengimplementasikan interface component secara langsung. - Composite (
Directory): Elemen kompleks yang memiliki anak (bisa berupa Leaf atau Composite lainnya). Elemen ini mengimplementasikan method component dengan mendelegasikan tugas ke anak-anaknya.
Skenario Kasus Penggunaan
Jika kamu sedang membangun modul sistem berkas atau mesin pembuat XML/HTML, kamu akan sering berurusan dengan komponen terstruktur pohon.
Tanpa Composite pattern, kamu harus menulis pengecekan kondisi (if-else) di mana-mana:
if node.IsDirectory() {
// Lakukan perulangan di dalam folder dan jumlahkan ukurannya
} else {
// Cukup kembalikan ukuran file
}
Hal ini menyebabkan kode menjadi berantakan dan melanggar Open/Closed Principle, karena menambahkan tipe node baru (seperti Symlink atau Shortcut) akan memaksamu mengubah semua fungsi penelusuran. Composite pattern mengeliminasi pengecekan ini dengan menyediakan interface yang seragam.
Implementasi Golang
Berikut adalah implementasi lengkap dan siap jalan dari Composite pattern dalam bahasa Go yang menyimulasikan sistem berkas.
package main
import (
"fmt"
)
// ==========================================
// 1. Interface Component
// ==========================================
// FileSystemNode mendefinisikan perilaku yang harus dimiliki oleh File maupun Directory.
type FileSystemNode interface {
Name() string
Size() int
Print(indent string)
}
// ==========================================
// 2. Leaf Component (File)
// ==========================================
// File merepresentasikan node daun (leaf) di dalam hierarki. Objek ini tidak memiliki anak.
type File struct {
name string
size int
}
func NewFile(name string, size int) *File {
return &File{name: name, size: size}
}
func (f *File) Name() string {
return f.name
}
func (f *File) Size() int {
return f.size
}
func (f *File) Print(indent string) {
fmt.Printf("%s📄 %s (%d bytes)\n", indent, f.name, f.size)
}
// ==========================================
// 3. Composite Component (Directory)
// ==========================================
// Directory merepresentasikan node komposit yang dapat menampung file atau directory lainnya.
type Directory struct {
name string
components []FileSystemNode
}
func NewDirectory(name string) *Directory {
return &Directory{
name: name,
components: make([]FileSystemNode, 0),
}
}
func (d *Directory) Name() string {
return d.name
}
// Add memasukkan file atau folder baru ke dalam direktori ini.
func (d *Directory) Add(node FileSystemNode) {
d.components = append(d.components, node)
}
// Size menghitung total ukuran direktori secara rekursif dengan menjumlahkan ukuran seluruh anak.
func (d *Directory) Size() int {
total := 0
for _, node := range d.components {
total += node.Size() // Panggilan rekursif
}
return total
}
func (d *Directory) Print(indent string) {
fmt.Printf("%s📁 %s/\n", indent, d.name)
for _, node := range d.components {
node.Print(indent + " ") // Cetak rekursif
}
}
// ==========================================
// 4. Eksekusi Client
// ==========================================
func main() {
// Membuat file individual (Leaves)
file1 := NewFile("resume.pdf", 1200)
file2 := NewFile("cover_letter.docx", 800)
file3 := NewFile("avatar.png", 5000)
file4 := NewFile("main.go", 1500)
// Membuat direktori utama (Root)
rootDir := NewDirectory("Home")
// Membuat sub-direktori (Composites)
docsDir := NewDirectory("Documents")
picsDir := NewDirectory("Pictures")
codeDir := NewDirectory("SourceCode")
// Menyusun struktur pohon
docsDir.Add(file1)
docsDir.Add(file2)
picsDir.Add(file3)
codeDir.Add(file4)
// Menambahkan sub-folder ke direktori root
rootDir.Add(docsDir)
rootDir.Add(picsDir)
rootDir.Add(codeDir)
// Menambahkan file langsung di root directory
readme := NewFile("README.md", 250)
rootDir.Add(readme)
// Menampilkan struktur direktori dan ukurannya secara seragam
fmt.Println("--- Struktur Pohon File System ---")
rootDir.Print("")
fmt.Println("\n--- Kalkulasi API yang Seragam ---")
fmt.Printf("Ukuran file tunggal '%s': %d bytes\n", file1.Name(), file1.Size())
fmt.Printf("Total Ukuran subfolder '%s': %d bytes\n", docsDir.Name(), docsDir.Size())
fmt.Printf("Total Ukuran seluruh direktori '%s': %d bytes\n", rootDir.Name(), rootDir.Size())
}
Ringkasan
Keuntungan
- Polimorfisme: Client dapat memperlakukan objek tunggal dan komposit secara seragam. Tidak perlu menulis percabangan kode untuk mencari tahu tipe objek.
- Kemudahan Ekstensi: Kamu bisa menambahkan tipe komponen baru ke dalam pohon tanpa mengubah kode client (Open/Closed Principle).
- Menyederhanakan Kode Client: Client tidak perlu memusingkan bagaimana proses rekursi di dalam struktur komposit berjalan.
Kerugian
- Generalisasi yang Berlebihan: Membuat interface yang terlalu umum terkadang memaksamu mendeklarasikan method kosong atau tidak didukung pada leaf node (misalnya jika kita menaruh method
Addlangsung di interfaceFileSystemNode, maka structFileterpaksa harus mengembalikan error atau panic). - Type Safety: Menjadi lebih sulit untuk membatasi jenis objek yang bisa dimasukkan ke dalam komposit jika interfacenya dibuat terlalu luas.
Kapan Harus Digunakan
- Ketika ingin merepresentasikan hierarki objek berbentuk pohon (seperti direktori file, struktur organisasi, atau pohon komponen UI).
- Ketika ingin kode client memperlakukan elemen sederhana (leaf) dan elemen kompleks (composite) secara sama tanpa membedakannya.