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:
- You need to represent part-whole hierarchies (file systems, organization charts, UI component trees)
- You want clients to treat individual objects and compositions uniformly
- You’re building recursive structures where operations should work at any level
- You need to implement tree-like structures with consistent interfaces
- You want to simplify client code by avoiding type-checking logic
Core Components
The pattern consists of three key elements:
- Component: Interface defining common operations for both simple and complex objects
- Leaf: Basic element with no children that implements the component interface
- 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
- Uniform Interface: Clients treat individual objects and compositions identically
- Open/Closed Principle: Easy to add new component types without changing existing code
- Recursive Operations: Operations naturally cascade through the tree structure
- Simplified Client Code: No need for type-checking or special-case logic
Disadvantages
- Overly General Design: Can make it harder to restrict component types in a composite
- Component Management Complexity: Tracking parent-child relationships can be tricky
- Performance: Recursive operations can be expensive for deep hierarchies
- Type Safety: Harder to enforce that composites only contain specific types
When NOT to Use
- Flat data structures with no hierarchical relationships
- When you need strict type control over what can be composed
- Simple lists where array/slice operations are sufficient
- When operations differ significantly between leaves and composites
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
- Clear Separation: Keep leaf and composite logic distinct
- Defensive Copying: Consider immutability for safer tree manipulation
- Lazy Evaluation: Cache expensive calculations (like size) when appropriate
- Iterator Support: Provide ways to traverse the structure efficiently
- Parent References: Consider whether children need to know their parents
- 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.