This post is part of a series of reviews on the book Design Patterns in Ruby. Check out the Introduction post for a full table of contents along with some generic principles regarding Design Patterns.

The motivation behind the use of the Composite pattern is to treat complex objects (that are made up by other objects) the same way you’d treat simple objects. The key analogy to this is to look at the Composite pattern as a tree, where you’d have branches that are made up by smaller branches which are made up by leaves, but they’d all share the same interface, so dealing with a branch or a leaf would be transparent:

To demonstrate the Composite pattern in action, let’s use an example of cake making. To actually make a cake, you need to do a ton of things, and most of the times these things involve alot of other things, so let’s just call these ‘things’ Tasks, which will be Leaf classes in the Composite diagram.

class Task
	attr_reader :name
	
	def initialize(name)
		@name = name
	end
	
	def get_time_required
		0.0
	end
end

The goal here is to figure out how long we’re gonna take to make a cake with a rake on a snake — HA!

Extremely funny jokes apart, let’s code a few simple tasks:

class AddIngredientsTask < Task
	def initialize
		super('Adds some ingredients.')
	end
	
	def get_time_required
		1.0
	end
end

class MixTask < Task
	def initialize
		super('Mixes the stuff.')
	end
	
	def get_time_required
		3.0
	end
end

class PutInOvenTask < Task
	def initialize
		super('Put the batter in the oven!')
	end
	
	def get_time_required
		0.5
	end
end

Now that we have our Leaf classes, we can create our Composite classes, which will be responsible for using these Tasks to create more complex tasks, but keeping that complexity transparent:

class CompositeTask < Task
	def initialize(name)
		super(name)
		@sub_tasks = []
	end
	
	def add_sub_task(task)
		@sub_tasks << task
	end
	
	def delete_sub_task(task)
		@sub_tasks.delete(task)
	end
	
	def get_time_required
		time = 0
		@sub_tasks.each{|task| time+=task.get_time_required}
		time
	end
end

Adding a couple of composite classes to finish up:

class MakeBatterTask < CompositeTask
	def initialize
		super('Make batter')
		add_sub_task(AddIngredientsTask.new)
		add_sub_task(MixTask.new)
	end
end

class MakeCakeTask < CompositeTask
	def initialize
		super('Make the whole cake!')
		add_sub_task(MakeBatterTask.new)
		add_sub_task(PutInOvenTask.new)
	end
end

And that’s all to it! Although MakeCakeTask is a super task that has a task with sub-tasks in it (MakeBatterTask), everything looks the same in the end since everyone shares the same interface. To know how much time our little baking took, just run:

puts MakeCakeTask.new.get_time_required

A cool Rubyism you can work with the Composite pattern is to use array operators on the CompositeTask methods:

def << (task)
	@sub_tasks << task
end

def [](index)
	@sub_tasks[index]
end

def []=(index, new_value)
	@sub_tasks[index] = new_value
end

This way we can now add tasks the same way we add elements to an array, along with the other array-like things we defined:

composite = CompositeTask.new('example')
composite << MixTask.new
puts composite[0]
composite[0] = PutInOvenTask.new

Just be careful not to start considering composite tasks a subclass of Array just because of the few method definitions we could save, Tasks are NOT a type of Array, so inheritance is completely off the table.