Overview
The Composite Pattern organizes objects into tree structures where individual objects and groups of objects share the same interface. This pattern addresses the problem of building hierarchical structures where both primitive elements and containers of elements must respond to the same operations. The pattern originates from the Gang of Four design patterns catalog and solves the complexity of handling nested structures by treating all nodes in the tree uniformly.
The pattern defines three primary roles: Component (the common interface), Leaf (primitive objects that cannot contain other objects), and Composite (container objects that hold other components). Clients interact with the component interface without knowing whether they work with a single object or a composition of objects. This eliminates conditional logic that checks object types and simplifies client code when working with recursive structures.
# Basic structure showing uniform treatment
class FileSystemItem
def size
raise NotImplementedError
end
end
class File < FileSystemItem
def initialize(name, bytes)
@name = name
@bytes = bytes
end
def size
@bytes
end
end
class Directory < FileSystemItem
def initialize(name)
@name = name
@children = []
end
def add(item)
@children << item
end
def size
@children.sum(&:size)
end
end
# Client treats both uniformly
root = Directory.new("root")
root.add(File.new("a.txt", 100))
root.add(File.new("b.txt", 200))
puts root.size # => 300
The pattern applies to domains where hierarchical structures naturally occur: file systems, organizational charts, graphical user interfaces, arithmetic expressions, and document structures. The uniform interface enables recursive algorithms to traverse and process entire structures without type checking at each level.
Key Principles
The Composite Pattern operates on three fundamental principles. First, the Component abstraction defines operations that apply to both simple and complex objects. This abstraction establishes the contract that all participants in the structure must fulfill. Second, recursive composition allows Composites to contain both Leaf objects and other Composites, creating arbitrary depth hierarchies. Third, location transparency means clients interact with the structure through the component interface without distinguishing between leaves and composites.
The Component interface declares operations for both leaf and composite behaviors. Common operations include adding and removing child components, accessing children, and performing the primary domain operations. Some operations make sense only for composites (like add or remove), while others apply to all nodes (like size or display). The design must address how to handle operations that do not apply to all node types.
# Component interface with both types of operations
module Component
def operation
raise NotImplementedError
end
def add(component)
raise NotImplementedError, "#{self.class} cannot add children"
end
def remove(component)
raise NotImplementedError, "#{self.class} cannot remove children"
end
def get_child(index)
raise NotImplementedError, "#{self.class} has no children"
end
end
Leaf objects represent terminal nodes in the structure. They implement the component interface but typically raise errors or return empty results for composite-specific operations. Leaf objects perform the actual work of the domain operations since they represent primitive elements that cannot delegate to children.
Composite objects implement component operations by delegating to their children. When a composite receives an operation request, it forwards the request to each child and may combine the results. This recursive delegation continues until reaching leaf nodes. Composites maintain a collection of child components and provide methods to add, remove, and access children.
# Leaf behavior - performs actual work
class TextElement
include Component
def initialize(text)
@text = text
end
def operation
"Text: #{@text}"
end
end
# Composite behavior - delegates to children
class Container
include Component
def initialize(name)
@name = name
@children = []
end
def operation
results = @children.map(&:operation)
"Container #{@name}: [#{results.join(', ')}]"
end
def add(component)
@children << component
end
def remove(component)
@children.delete(component)
end
def get_child(index)
@children[index]
end
end
# Building and operating on structure
root = Container.new("root")
root.add(TextElement.new("A"))
branch = Container.new("branch")
branch.add(TextElement.new("B"))
branch.add(TextElement.new("C"))
root.add(branch)
puts root.operation
# => Container root: [Text: A, Container branch: [Text: B, Text: C]]
The pattern supports two design approaches for the component interface. The transparent approach declares all operations (including add, remove, get_child) in the component interface, making leaves and composites fully interchangeable but requiring leaves to implement operations that do not make sense for them. The safe approach declares only operations applicable to all nodes in the component interface, with composite-specific operations defined only in the Composite class, providing type safety at the cost of losing uniform treatment for structural operations.
Ruby Implementation
Ruby implements the Composite Pattern using modules for the component interface and classes for leaf and composite implementations. Modules define the common interface through method declarations that raise NotImplementedError, establishing the contract without inheritance constraints. Classes include the module and provide concrete implementations.
module GraphicComponent
def draw
raise NotImplementedError
end
def bounds
raise NotImplementedError
end
end
class Circle
include GraphicComponent
def initialize(x, y, radius)
@x, @y, @radius = x, y, radius
end
def draw
"Drawing circle at (#{@x}, #{@y}) with radius #{@radius}"
end
def bounds
{ x: @x - @radius, y: @y - @radius,
width: @radius * 2, height: @radius * 2 }
end
end
class Rectangle
include GraphicComponent
def initialize(x, y, width, height)
@x, @y, @width, @height = x, y, width, height
end
def draw
"Drawing rectangle at (#{@x}, #{@y}) sized #{@width}x#{@height}"
end
def bounds
{ x: @x, y: @y, width: @width, height: @height }
end
end
class Group
include GraphicComponent
def initialize
@components = []
end
def add(component)
@components << component
self
end
def remove(component)
@components.delete(component)
self
end
def draw
drawings = @components.map(&:draw)
"Group: [#{drawings.join(', ')}]"
end
def bounds
return { x: 0, y: 0, width: 0, height: 0 } if @components.empty?
all_bounds = @components.map(&:bounds)
min_x = all_bounds.map { |b| b[:x] }.min
min_y = all_bounds.map { |b| b[:y] }.min
max_x = all_bounds.map { |b| b[:x] + b[:width] }.max
max_y = all_bounds.map { |b| b[:y] + b[:height] }.max
{ x: min_x, y: min_y, width: max_x - min_x, height: max_y - min_y }
end
end
# Using the graphics hierarchy
canvas = Group.new
canvas.add(Circle.new(10, 10, 5))
canvas.add(Rectangle.new(20, 20, 30, 40))
nested_group = Group.new
nested_group.add(Circle.new(100, 100, 10))
nested_group.add(Circle.new(120, 100, 10))
canvas.add(nested_group)
puts canvas.draw
puts canvas.bounds.inspect
Ruby's Enumerable module integrates naturally with composite implementations. Composites can include Enumerable and implement the each method to enable iteration over children. This provides access to map, select, reduce, and other iteration methods without additional code.
class CompositeTask
include Enumerable
def initialize(name)
@name = name
@subtasks = []
end
def add(task)
@subtasks << task
self
end
def each(&block)
@subtasks.each(&block)
end
def time_estimate
@subtasks.sum(&:time_estimate)
end
def complete?
@subtasks.all?(&:complete?)
end
end
class Task
attr_reader :name, :time_estimate
def initialize(name, time_estimate, complete = false)
@name = name
@time_estimate = time_estimate
@complete = complete
end
def complete?
@complete
end
end
project = CompositeTask.new("Project")
project.add(Task.new("Design", 8))
project.add(Task.new("Implementation", 20))
testing = CompositeTask.new("Testing")
testing.add(Task.new("Unit tests", 5))
testing.add(Task.new("Integration tests", 8))
project.add(testing)
# Using Enumerable methods
puts "Total time: #{project.time_estimate} hours"
puts "Incomplete tasks: #{project.select { |t| !t.complete? }.map(&:name)}"
puts "All complete? #{project.complete?}"
Ruby blocks and procs provide flexible traversal mechanisms for composite structures. Implementing traversal methods that accept blocks allows clients to process nodes during tree walks without knowing the structure's implementation details.
class Node
attr_reader :name
def initialize(name)
@name = name
@children = []
end
def add(node)
@children << node
self
end
# Pre-order traversal with block
def traverse_preorder(&block)
block.call(self)
@children.each { |child| child.traverse_preorder(&block) }
end
# Post-order traversal with block
def traverse_postorder(&block)
@children.each { |child| child.traverse_postorder(&block) }
block.call(self)
end
# Level-order traversal
def traverse_levelorder(&block)
queue = [self]
until queue.empty?
current = queue.shift
block.call(current)
queue.concat(current.instance_variable_get(:@children))
end
end
# Find nodes matching condition
def find_all(&predicate)
results = []
traverse_preorder do |node|
results << node if predicate.call(node)
end
results
end
end
# Building and traversing tree
root = Node.new("root")
root.add(Node.new("a")).add(Node.new("b"))
branch = Node.new("branch")
branch.add(Node.new("c")).add(Node.new("d"))
root.add(branch)
puts "Pre-order:"
root.traverse_preorder { |n| puts " #{n.name}" }
puts "Post-order:"
root.traverse_postorder { |n| puts " #{n.name}" }
puts "Find nodes with 'a' in name:"
root.find_all { |n| n.name.include?('a') }.each do |n|
puts " #{n.name}"
end
Practical Examples
A menu system demonstrates composite structures where menu items and submenus share a common interface. Individual menu items represent leaf nodes that execute actions when selected. Submenus represent composite nodes that contain other menu items or nested submenus.
class MenuItem
attr_reader :label
def initialize(label, &action)
@label = label
@action = action
end
def select
@action.call if @action
end
def display(indent = 0)
puts "#{' ' * indent}#{@label}"
end
def find(label)
@label == label ? self : nil
end
end
class Menu
attr_reader :label
def initialize(label)
@label = label
@items = []
end
def add(item)
@items << item
self
end
def select
display
end
def display(indent = 0)
puts "#{' ' * indent}#{@label}"
@items.each { |item| item.display(indent + 1) }
end
def find(label)
return self if @label == label
@items.each do |item|
found = item.find(label)
return found if found
end
nil
end
end
# Building menu hierarchy
main_menu = Menu.new("Main Menu")
file_menu = Menu.new("File")
file_menu.add(MenuItem.new("New") { puts "Creating new file" })
file_menu.add(MenuItem.new("Open") { puts "Opening file" })
file_menu.add(MenuItem.new("Save") { puts "Saving file" })
main_menu.add(file_menu)
edit_menu = Menu.new("Edit")
edit_menu.add(MenuItem.new("Cut") { puts "Cutting" })
edit_menu.add(MenuItem.new("Copy") { puts "Copying" })
edit_menu.add(MenuItem.new("Paste") { puts "Pasting" })
main_menu.add(edit_menu)
main_menu.add(MenuItem.new("Exit") { puts "Exiting application" })
# Using the menu system
main_menu.display
found = main_menu.find("Copy")
found&.select # => Copying
An arithmetic expression evaluator represents mathematical expressions as trees where numbers are leaves and operators are composites. Each operator node contains operand sub-expressions that may themselves be complex expressions or simple numbers.
class Expression
def evaluate
raise NotImplementedError
end
def to_s
raise NotImplementedError
end
end
class Number < Expression
def initialize(value)
@value = value
end
def evaluate
@value
end
def to_s
@value.to_s
end
end
class BinaryOperation < Expression
def initialize(left, right)
@left = left
@right = right
end
end
class Addition < BinaryOperation
def evaluate
@left.evaluate + @right.evaluate
end
def to_s
"(#{@left} + #{@right})"
end
end
class Multiplication < BinaryOperation
def evaluate
@left.evaluate * @right.evaluate
end
def to_s
"(#{@left} * #{@right})"
end
end
class Subtraction < BinaryOperation
def evaluate
@left.evaluate - @right.evaluate
end
def to_s
"(#{@left} - #{@right})"
end
end
class Division < BinaryOperation
def evaluate
denominator = @right.evaluate
raise ZeroDivisionError, "Division by zero" if denominator == 0
@left.evaluate.to_f / denominator
end
def to_s
"(#{@left} / #{@right})"
end
end
# Building expression: ((5 + 3) * 2) - (10 / 2)
left_side = Multiplication.new(
Addition.new(Number.new(5), Number.new(3)),
Number.new(2)
)
right_side = Division.new(Number.new(10), Number.new(2))
expression = Subtraction.new(left_side, right_side)
puts expression.to_s # => (((5 + 3) * 2) - (10 / 2))
puts expression.evaluate # => 11.0
A document structure with sections, paragraphs, and text elements models hierarchical documents where sections contain other sections or content elements. This structure supports operations like word counting, rendering, and search across the entire document through uniform interfaces.
class DocumentElement
def word_count
raise NotImplementedError
end
def render
raise NotImplementedError
end
def search(term)
[]
end
end
class Text < DocumentElement
attr_reader :content
def initialize(content)
@content = content
end
def word_count
@content.split.length
end
def render
@content
end
def search(term)
@content.downcase.include?(term.downcase) ? [self] : []
end
end
class Paragraph < DocumentElement
def initialize
@elements = []
end
def add(element)
@elements << element
self
end
def word_count
@elements.sum(&:word_count)
end
def render
@elements.map(&:render).join(' ')
end
def search(term)
@elements.flat_map { |e| e.search(term) }
end
end
class Section < DocumentElement
attr_reader :title
def initialize(title)
@title = title
@contents = []
end
def add(content)
@contents << content
self
end
def word_count
@contents.sum(&:word_count)
end
def render
rendered_contents = @contents.map(&:render).join("\n\n")
"# #{@title}\n\n#{rendered_contents}"
end
def search(term)
results = @title.downcase.include?(term.downcase) ? [self] : []
results + @contents.flat_map { |c| c.search(term) }
end
end
# Building document structure
doc = Section.new("Software Design Patterns")
intro = Paragraph.new
intro.add(Text.new("Design patterns provide reusable solutions"))
intro.add(Text.new("to common software design problems."))
doc.add(intro)
subsection = Section.new("Structural Patterns")
para = Paragraph.new
para.add(Text.new("Structural patterns describe how classes"))
para.add(Text.new("and objects combine to form larger structures."))
subsection.add(para)
doc.add(subsection)
puts "Total words: #{doc.word_count}"
puts "\nRendered document:"
puts doc.render
puts "\nSearch for 'patterns':"
doc.search('patterns').each { |e| puts " Found in: #{e.class}" }
Design Considerations
The Composite Pattern applies when the domain naturally contains hierarchical part-whole relationships and clients should treat individual objects and compositions uniformly. File systems, organizational structures, graphical scenes, and nested data structures benefit from this pattern. The decision to use composites depends on whether the benefit of uniform treatment outweighs the complexity of maintaining tree structures.
The transparent approach places all operations in the component interface, including add, remove, and child access methods. This achieves maximum uniformity since clients can call any operation on any component without type checking. Leaf implementations must handle composite-specific operations, typically by raising errors or returning nil. This approach trades type safety for simplicity in client code.
# Transparent approach - all operations in component
module Component
def operation
raise NotImplementedError
end
def add(component)
raise "Cannot add to #{self.class}"
end
def remove(component)
raise "Cannot remove from #{self.class}"
end
def children
[]
end
end
class Leaf
include Component
def operation
"Leaf operation"
end
# Inherits add/remove that raise errors
end
class Composite
include Component
def initialize
@children = []
end
def operation
@children.map(&:operation).join(', ')
end
def add(component)
@children << component
self
end
def remove(component)
@children.delete(component)
self
end
def children
@children.dup
end
end
# Client code treats all nodes uniformly
def process(component)
puts component.operation
component.children.each { |child| process(child) }
end
The safe approach restricts the component interface to operations that make sense for all nodes. Only composites expose structural operations like add and remove. This provides type safety at compile time (or with Ruby's duck typing, at runtime) but requires clients to check types before performing composite-specific operations. This approach prevents errors from calling invalid operations on leaves.
# Safe approach - minimal component interface
module Component
def operation
raise NotImplementedError
end
end
class SafeLeaf
include Component
def operation
"Leaf operation"
end
end
class SafeComposite
include Component
def initialize
@children = []
end
def operation
@children.map(&:operation).join(', ')
end
# Structural operations only in Composite
def add(component)
@children << component
self
end
def remove(component)
@children.delete(component)
self
end
def children
@children.dup
end
end
# Client must check type for structural operations
def add_to_composite(target, component)
if target.respond_to?(:add)
target.add(component)
else
raise "Cannot add to #{target.class}"
end
end
The pattern compares with the Decorator Pattern, which also uses recursive composition. Decorators focus on adding responsibilities to objects while maintaining the same interface, typically with one child component. Composites focus on representing part-whole hierarchies with potentially many children. The Iterator Pattern often complements composites by providing standard traversal mechanisms without exposing internal structure.
Performance considerations include the overhead of traversing potentially deep hierarchies for operations that aggregate results from all nodes. Caching computed values at composite nodes reduces repeated calculations when the structure remains unchanged. Memory overhead from maintaining parent references or child collections scales with tree size and depth.
Common Patterns
The caching pattern stores computed results at composite nodes to avoid repeated traversal when the structure remains static. Composites invalidate cached values when children change, either immediately or lazily on next access. This pattern significantly improves performance for frequently accessed aggregate values in stable structures.
class CachedComposite
def initialize(name)
@name = name
@children = []
@cached_size = nil
end
def add(component)
@children << component
invalidate_cache
self
end
def remove(component)
@children.delete(component)
invalidate_cache
self
end
def size
return @cached_size if @cached_size
@cached_size = @children.sum(&:size)
end
private
def invalidate_cache
@cached_size = nil
end
end
class Item
def initialize(size)
@size = size
end
def size
@size
end
end
container = CachedComposite.new("root")
container.add(Item.new(100))
container.add(Item.new(200))
puts container.size # Computes: 300
puts container.size # Returns cached: 300
container.add(Item.new(50))
puts container.size # Recomputes: 350
The parent reference pattern adds a parent pointer to each component, enabling upward traversal from any node to the root. This supports operations that need context from ancestors, such as computing absolute positions or propagating changes up the tree. Parent references require careful management during add and remove operations to maintain consistency.
class ComponentWithParent
attr_reader :name
attr_accessor :parent
def initialize(name)
@name = name
@parent = nil
end
def path
parts = [name]
current = parent
while current
parts.unshift(current.name)
current = current.parent
end
parts.join('/')
end
def root
current = self
current = current.parent while current.parent
current
end
end
class CompositeWithParent < ComponentWithParent
def initialize(name)
super
@children = []
end
def add(component)
component.parent = self
@children << component
self
end
def remove(component)
component.parent = nil
@children.delete(component)
self
end
end
root = CompositeWithParent.new("root")
branch = CompositeWithParent.new("branch")
leaf = ComponentWithParent.new("leaf")
root.add(branch)
branch.add(leaf)
puts leaf.path # => root/branch/leaf
puts leaf.root.name # => root
The visitor pattern combines with composites to add operations without modifying component classes. Components accept visitors and forward them to children. Visitors implement operations as visit methods for different component types. This separation allows adding new operations by creating new visitors rather than modifying the composite structure.
class VisitableComponent
def accept(visitor)
raise NotImplementedError
end
end
class VisitableLeaf < VisitableComponent
attr_reader :data
def initialize(data)
@data = data
end
def accept(visitor)
visitor.visit_leaf(self)
end
end
class VisitableComposite < VisitableComponent
attr_reader :name, :children
def initialize(name)
@name = name
@children = []
end
def add(component)
@children << component
self
end
def accept(visitor)
visitor.visit_composite(self)
@children.each { |child| child.accept(visitor) }
end
end
class PrintVisitor
def initialize
@indent = 0
end
def visit_leaf(leaf)
puts "#{' ' * @indent}Leaf: #{leaf.data}"
end
def visit_composite(composite)
puts "#{' ' * @indent}Composite: #{composite.name}"
@indent += 1
yield if block_given?
@indent -= 1
end
end
class CountVisitor
attr_reader :leaf_count, :composite_count
def initialize
@leaf_count = 0
@composite_count = 0
end
def visit_leaf(leaf)
@leaf_count += 1
end
def visit_composite(composite)
@composite_count += 1
end
end
# Building and visiting structure
root = VisitableComposite.new("root")
root.add(VisitableLeaf.new("A"))
branch = VisitableComposite.new("branch")
branch.add(VisitableLeaf.new("B"))
branch.add(VisitableLeaf.new("C"))
root.add(branch)
printer = PrintVisitor.new
root.accept(printer)
counter = CountVisitor.new
root.accept(counter)
puts "Leaves: #{counter.leaf_count}, Composites: #{counter.composite_count}"
The chain of responsibility pattern integrates with composites when requests should propagate up the tree until handled. Each node attempts to handle the request, and if unable, forwards it to its parent. This supports event bubbling in UI hierarchies or command processing in organizational structures.
class ChainComponent
attr_accessor :parent
def initialize(name)
@name = name
@parent = nil
end
def handle(request)
if can_handle?(request)
process(request)
elsif @parent
@parent.handle(request)
else
puts "#{request} not handled"
end
end
def can_handle?(request)
false
end
def process(request)
puts "#{@name} handles #{request}"
end
end
class SpecificHandler < ChainComponent
def can_handle?(request)
request.start_with?(@name)
end
end
class ChainComposite < ChainComponent
def initialize(name)
super
@children = []
end
def add(component)
component.parent = self
@children << component
self
end
end
# Building chain hierarchy
root = ChainComposite.new("root")
manager = SpecificHandler.new("manager")
developer = SpecificHandler.new("developer")
root.add(manager)
manager.parent = root
root.add(developer)
developer.parent = root
developer.handle("developer_task") # => developer handles developer_task
developer.handle("manager_task") # => manager handles manager_task
developer.handle("other_task") # => other_task not handled
Common Pitfalls
The maximum uniformity pitfall occurs when placing all operations in the component interface, including those that apply only to composites. This forces leaf classes to implement methods like add or remove that make no sense for leaves. The resulting implementations either raise exceptions or silently fail, deferring type errors from compile time to runtime. This approach prioritizes uniform treatment over type safety.
# Pitfall: Leaf must implement composite operations
class ProblemLeaf
def add(component)
raise "Cannot add to leaf" # Runtime error
end
end
# Better: Use safe approach or document transparent tradeoff
class SafeLeaf
# No add method - type error at call site
end
class TransparentLeaf
def add(component)
# Explicitly documented that this raises
raise NotImplementedError, "Leaves cannot contain children"
end
end
The parent reference inconsistency pitfall happens when composite modification methods fail to maintain parent references. Adding a component without setting its parent or removing without clearing the parent creates orphaned references. These inconsistencies cause incorrect results from operations that traverse parent chains.
# Pitfall: Forgetting to manage parent references
class BrokenComposite
def add(component)
@children << component
# Missing: component.parent = self
end
def remove(component)
@children.delete(component)
# Missing: component.parent = nil
end
end
# Better: Always manage parent references
class ProperComposite
def add(component)
remove_from_old_parent(component)
component.parent = self
@children << component
end
def remove(component)
return unless @children.delete(component)
component.parent = nil
end
private
def remove_from_old_parent(component)
component.parent.remove(component) if component.parent
end
end
The shared component pitfall occurs when adding the same component to multiple composites. Most composite implementations assume exclusive ownership of children. Sharing a component between parents creates ambiguous parent references and leads to unexpected behavior during traversal or deletion. Cloning components before adding to multiple parents avoids this issue.
# Pitfall: Sharing component between parents
shared = Leaf.new("shared")
composite1.add(shared)
composite2.add(shared) # Now shared.parent points to composite2 only
# Better: Clone before adding to second parent
shared = Leaf.new("shared")
composite1.add(shared)
composite2.add(shared.dup)
# Or: Explicitly support shared components with parent collection
class MultiParentComponent
def initialize
@parents = []
end
def add_parent(parent)
@parents << parent unless @parents.include?(parent)
end
def remove_parent(parent)
@parents.delete(parent)
end
def parents
@parents.dup
end
end
The deep hierarchy performance pitfall emerges when recursive operations traverse very deep trees repeatedly. Each operation that aggregates values from all nodes walks the entire subtree. For large or deep structures, this creates performance problems. Caching aggregate values and invalidating caches on modification improves performance significantly.
# Pitfall: Repeated deep traversal
class SlowComposite
def size
@children.sum(&:size) # Recomputes every call
end
end
# Better: Cache with invalidation
class FastComposite
def initialize
@children = []
@size_cache = nil
end
def add(component)
@children << component
invalidate_size_cache
component.on_change { invalidate_size_cache }
end
def size
@size_cache ||= compute_size
end
private
def compute_size
@children.sum(&:size)
end
def invalidate_size_cache
@size_cache = nil
@parent&.invalidate_size_cache # Propagate invalidation
end
end
The circular reference pitfall happens when adding a composite to one of its descendants, creating a cycle in the tree. Traversal operations on cyclic structures loop infinitely. Preventing cycles requires checking during add operations whether the component being added contains the target composite in its subtree.
# Pitfall: Creating cycles
composite1.add(composite2)
composite2.add(composite1) # Creates cycle
# Better: Check for cycles before adding
class SafeComposite
def add(component)
raise ArgumentError, "Cannot add ancestor" if contains?(component)
@children << component
component.parent = self
end
def contains?(component)
return true if equal?(component)
@children.any? do |child|
child.respond_to?(:contains?) && child.contains?(component)
end
end
end
The memory leak pitfall occurs with parent references when components removed from trees retain their parent pointers. These orphaned references prevent garbage collection of the parent structure. Clearing parent references during removal ensures proper cleanup.
# Pitfall: Not clearing parent reference
def buggy_remove(component)
@children.delete(component)
# Parent reference remains, preventing GC
end
# Better: Clear parent on removal
def proper_remove(component)
return false unless @children.delete(component)
component.parent = nil
true
end
Reference
Component Pattern Structure
| Element | Responsibility | Implementation |
|---|---|---|
| Component | Declares interface for objects in composition | Module with abstract methods |
| Leaf | Represents leaf objects with no children | Includes Component, implements operations |
| Composite | Defines behavior for components with children | Includes Component, manages children |
| Client | Manipulates objects through Component interface | Uses Component methods only |
Common Operations
| Operation | Purpose | Leaf Behavior | Composite Behavior |
|---|---|---|---|
| operation | Perform primary domain action | Execute directly | Delegate to children, combine results |
| add | Add child component | Raise error or no-op | Add to children collection |
| remove | Remove child component | Raise error or no-op | Remove from children collection |
| get_child | Access child by index | Raise error or return nil | Return child at index |
| parent | Access parent component | Return parent reference | Return parent reference |
Design Approaches
| Approach | Component Interface | Type Safety | Client Code | Use When |
|---|---|---|---|---|
| Transparent | All operations including add/remove | Low - runtime errors | Simple - uniform treatment | Uniformity more important than safety |
| Safe | Only common operations | High - compile/duck-type errors | Complex - type checking needed | Type safety more important than uniformity |
Traversal Strategies
| Strategy | Order | Implementation | Use Case |
|---|---|---|---|
| Pre-order | Parent before children | Process node, recurse on children | Directory listings, expression prefix notation |
| Post-order | Children before parent | Recurse on children, process node | Deletion, expression postfix notation |
| Level-order | Level by level | Use queue for breadth-first | Minimum depth, level-wise processing |
| Depth-first | Deepest first | Use stack or recursion | Path finding, exhaustive search |
Performance Optimization Techniques
| Technique | Benefit | Cost | When to Use |
|---|---|---|---|
| Result caching | Avoid repeated traversal | Memory overhead, invalidation complexity | Stable structures with frequent reads |
| Lazy evaluation | Defer expensive computations | Complexity in cache management | Infrequently accessed aggregates |
| Parent references | Enable upward traversal | Memory per node, consistency management | Need ancestor access or path computation |
| Iterative traversal | Avoid recursion overhead | More complex code | Very deep trees, stack limit concerns |
Common Method Signatures
# Component interface
module Component
def operation; end
def add(component); end
def remove(component); end
def get_child(index); end
def parent; end
def parent=(value); end
end
# Composite implementation
class Composite
def initialize; end
def add(component); end
def remove(component); end
def get_child(index); end
def each(&block); end
def operation; end
end
# Leaf implementation
class Leaf
def initialize(*args); end
def operation; end
end
# Traversal methods
def traverse_preorder(&block); end
def traverse_postorder(&block); end
def traverse_levelorder(&block); end
def find_all(&predicate); end
Integration Patterns
| Pattern | Combination Benefit | Implementation Notes |
|---|---|---|
| Iterator | Standard traversal mechanism | Include Enumerable, implement each |
| Visitor | Separate operations from structure | Accept visitor, forward to children |
| Chain of Responsibility | Request propagation | Handle or delegate to parent |
| Decorator | Add responsibilities | Wrap component, forward operations |
| Observer | Change notification | Notify observers on structural changes |
Ruby-Specific Idioms
| Idiom | Purpose | Example Usage |
|---|---|---|
| Module inclusion | Define component interface | include ComponentModule |
| Enumerable | Iteration support | include Enumerable; def each |
| Block traversal | Flexible tree walking | traverse { block } |
| Method chaining | Fluent interface | add(x).add(y).add(z) |
| Duck typing | Flexible polymorphism | respond_to?(:add) |