Composite Pattern for Hierarchical Structures in Go

Composite Pattern for Hierarchical Structures

The Composite pattern is a structural design pattern that lets you compose objects into tree structures and work with them as if they were individual objects. This pattern is particularly powerful for building systems with recursive hierarchical relationships where you want to treat individual objects and compositions uniformly.

When to Use the Composite Pattern

The Composite pattern is ideal when:

Core Components

The pattern consists of three key elements:

  1. Component: Interface defining common operations for both simple and complex objects
  2. Leaf: Basic element with no children that implements the component interface
  3. Composite: Element that can contain children (both leaves and other composites)

Implementation in Go

Let’s implement a file system example demonstrating the Composite pattern:

package main

import (
    "fmt"
    "strings"
)

// Component - common interface
type FileSystemNode interface {
    Name() string
    Size() int64
    Print(indent string)
}

// Leaf - File
type File struct {
    name string
    size int64
}

func NewFile(name string, size int64) *File {
    return &File{name: name, size: size}
}

func (f *File) Name() string {
    return f.name
}

func (f *File) Size() int64 {
    return f.size
}

func (f *File) Print(indent string) {
    fmt.Printf("%s- %s (%d bytes)\n", indent, f.name, f.size)
}

// Composite - Directory
type Directory struct {
    name     string
    children []FileSystemNode
}

func NewDirectory(name string) *Directory {
    return &Directory{
        name:     name,
        children: make([]FileSystemNode, 0),
    }
}

func (d *Directory) Name() string {
    return d.name
}

func (d *Directory) Size() int64 {
    var total int64
    for _, child := range d.children {
        total += child.Size()
    }
    return total
}

func (d *Directory) Add(node FileSystemNode) {
    d.children = append(d.children, node)
}

func (d *Directory) Remove(name string) {
    for i, child := range d.children {
        if child.Name() == name {
            d.children = append(d.children[:i], d.children[i+1:]...)
            return
        }
    }
}

func (d *Directory) Print(indent string) {
    fmt.Printf("%s+ %s/ (%d bytes total)\n", indent, d.name, d.Size())
    for _, child := range d.children {
        child.Print(indent + "  ")
    }
}

// Client code
func main() {
    // Create file structure
    root := NewDirectory("root")
    
    home := NewDirectory("home")
    home.Add(NewFile("profile.txt", 256))
    home.Add(NewFile("settings.json", 512))
    
    docs := NewDirectory("documents")
    docs.Add(NewFile("resume.pdf", 4096))
    docs.Add(NewFile("cover_letter.docx", 2048))
    
    home.Add(docs)
    root.Add(home)
    root.Add(NewFile("README.md", 1024))
    
    // Work with the structure uniformly
    root.Print("")
    fmt.Printf("\nTotal size: %d bytes\n", root.Size())
}

Output:

+ root/ (7936 bytes total)
  + home/ (6912 bytes total)
    - profile.txt (256 bytes)
    - settings.json (512 bytes)
    + documents/ (6144 bytes total)
      - resume.pdf (4096 bytes)
      - cover_letter.docx (2048 bytes)
  - README.md (1024 bytes)

Total size: 7936 bytes

Implementation in Python

Python’s duck typing makes the Composite pattern even more elegant:

from abc import ABC, abstractmethod
from typing import List

class FileSystemNode(ABC):
    """Component interface"""
    
    @abstractmethod
    def name(self) -> str:
        pass
    
    @abstractmethod
    def size(self) -> int:
        pass
    
    @abstractmethod
    def print(self, indent: str = ""):
        pass

class File(FileSystemNode):
    """Leaf implementation"""
    
    def __init__(self, name: str, size: int):
        self._name = name
        self._size = size
    
    def name(self) -> str:
        return self._name
    
    def size(self) -> int:
        return self._size
    
    def print(self, indent: str = ""):
        print(f"{indent}- {self._name} ({self._size} bytes)")

class Directory(FileSystemNode):
    """Composite implementation"""
    
    def __init__(self, name: str):
        self._name = name
        self._children: List[FileSystemNode] = []
    
    def name(self) -> str:
        return self._name
    
    def size(self) -> int:
        return sum(child.size() for child in self._children)
    
    def add(self, node: FileSystemNode):
        self._children.append(node)
    
    def remove(self, name: str):
        self._children = [c for c in self._children if c.name() != name]
    
    def print(self, indent: str = ""):
        print(f"{indent}+ {self._name}/ ({self.size()} bytes total)")
        for child in self._children:
            child.print(indent + "  ")

# Usage
if __name__ == "__main__":
    root = Directory("root")
    home = Directory("home")
    home.add(File("profile.txt", 256))
    home.add(File("settings.json", 512))
    
    root.add(home)
    root.add(File("README.md", 1024))
    
    root.print()

ReactJS Component Tree Example

The Composite pattern is fundamental to React’s component architecture:

// Component interface (base)
interface UIComponent {
  render(): React.ReactElement;
  getSize(): number;
}

// Leaf - Simple Button
class Button implements UIComponent {
  constructor(private label: string) {}
  
  render(): React.ReactElement {
    return <button>{this.label}</button>;
  }
  
  getSize(): number {
    return 1; // One element
  }
}

// Composite - Panel containing children
class Panel implements UIComponent {
  private children: UIComponent[] = [];
  
  constructor(private title: string) {}
  
  add(component: UIComponent): void {
    this.children.push(component);
  }
  
  remove(component: UIComponent): void {
    const index = this.children.indexOf(component);
    if (index > -1) {
      this.children.splice(index, 1);
    }
  }
  
  render(): React.ReactElement {
    return (
      <div className="panel">
        <h3>{this.title}</h3>
        <div className="panel-content">
          {this.children.map((child, idx) => (
            <div key={idx}>{child.render()}</div>
          ))}
        </div>
      </div>
    );
  }
  
  getSize(): number {
    return 1 + this.children.reduce((sum, child) => sum + child.getSize(), 0);
  }
}

// Usage
const mainPanel = new Panel("Main Panel");
mainPanel.add(new Button("Save"));
mainPanel.add(new Button("Cancel"));

const subPanel = new Panel("Settings");
subPanel.add(new Button("Apply"));

mainPanel.add(subPanel);

// Render the entire tree
const App = () => mainPanel.render();

Advanced Pattern: Composite with Visitor

Combine Composite with the Visitor pattern for operations across the tree:

// Visitor interface
type FileSystemVisitor interface {
    VisitFile(*File)
    VisitDirectory(*Directory)
}

// Size calculator visitor
type SizeCalculator struct {
    totalSize int64
}

func (sc *SizeCalculator) VisitFile(f *File) {
    sc.totalSize += f.Size()
}

func (sc *SizeCalculator) VisitDirectory(d *Directory) {
    for _, child := range d.children {
        child.Accept(sc)
    }
}

// Update FileSystemNode interface
type FileSystemNode interface {
    Name() string
    Size() int64
    Print(indent string)
    Accept(visitor FileSystemVisitor)
}

// Implement Accept methods
func (f *File) Accept(visitor FileSystemVisitor) {
    visitor.VisitFile(f)
}

func (d *Directory) Accept(visitor FileSystemVisitor) {
    visitor.VisitDirectory(d)
}

Trade-offs and Considerations

Advantages

  1. Uniform Interface: Clients treat individual objects and compositions identically
  2. Open/Closed Principle: Easy to add new component types without changing existing code
  3. Recursive Operations: Operations naturally cascade through the tree structure
  4. Simplified Client Code: No need for type-checking or special-case logic

Disadvantages

  1. Overly General Design: Can make it harder to restrict component types in a composite
  2. Component Management Complexity: Tracking parent-child relationships can be tricky
  3. Performance: Recursive operations can be expensive for deep hierarchies
  4. Type Safety: Harder to enforce that composites only contain specific types

When NOT to Use

Real-World Applications

1. UI Component Libraries

React, Vue, and other frameworks use the Composite pattern for component trees. Each component can contain other components, and operations like rendering, state updates, and event handling work uniformly.

2. File Systems

Operating systems represent directories and files using the Composite pattern. Operations like size calculation, permission changes, and searches work on both individual files and entire directory trees.

3. Organization Hierarchies

Companies, military structures, and org charts naturally fit the Composite pattern. Operations like calculating total headcount, budget allocation, or message broadcasting work at any level.

4. Graphics and Drawing Systems

Vector graphics applications use composites to group shapes. Operations like move, scale, and rotate work on both individual shapes and groups.

5. Expression Trees

Compilers and interpreters use the Composite pattern for abstract syntax trees (ASTs). Evaluation, optimization, and code generation work uniformly on simple expressions and complex compound expressions.

Best Practices

  1. Clear Separation: Keep leaf and composite logic distinct
  2. Defensive Copying: Consider immutability for safer tree manipulation
  3. Lazy Evaluation: Cache expensive calculations (like size) when appropriate
  4. Iterator Support: Provide ways to traverse the structure efficiently
  5. Parent References: Consider whether children need to know their parents
  6. Thread Safety: Add synchronization if the structure is modified concurrently

Conclusion

The Composite pattern is essential for working with hierarchical structures in a clean, uniform way. It shines in scenarios where you need to treat individual objects and collections uniformly, enabling recursive operations and simplifying client code. While it introduces some complexity in terms of type safety and component management, the benefits of a unified interface and extensible design make it invaluable for tree-structured data in Go, Python, and ReactJS applications.