Ruby Design Pattern: Composite Method

Swati Panhalkar
4 min readJan 12, 2018

--

The composite design pattern is a structural pattern used to represent objects that have a hierarchical tree structure. It allows for the uniform treatment of both individual leaf nodes and of branches composed of many nodes.

Here’s what wikipedia says about the composite pattern:

In software engineering, the composite pattern is a partitioning design pattern. The composite pattern describes that a group of objects are to be treated in the same way as a single instance of an object. The intent of a composite is to “compose” objects into tree structures to represent part-whole hierarchies. Implementing the composite pattern lets clients treat individual objects and compositions uniformly.

Creating Composites

To build the composite pattern we need three moving parts.

  1. A common interface or base class for all of your objects
  2. One or more leaf classes
  3. At least one higher-level class, called a composite class

1. A common interface:

When thinking about the design of a common interface, we should think “what will my basic and higher-level objects all have in common?” This is considered an interface or base class of the component.

2. Leaf classes:

These are the simple, indivisible building blocks of the process. If we were making a pizza, you could consider each ingredient (i.e. cheese, tomato sauce and flour) as a simple enough division to be considered a building block. These indivisible leaf classes should implement the component interface. For instance, a higher level component could be ingredients, preparation, creation, packaging, and delivery.

3. Higher-Level Class:

We need at least one higher-level class, which will be considered the composite of the leaf classes we went over above. The composite is a component, but its also a higher-level object that is built from subcomponents in a manner consistent with the law of demeter.
Worded a little differently, composites are just complex tasks made up of subtasks.

Example:

We’ve been asked to build a system that keeps track of the manufacturing of cakes, being a key requirement being able to know how long it takes the task of baking it. Making a cake is a complicated process, as it involves multiple tasks that might be composed of different subtasks. The whole process could be represented in the following tree:

|__ Manufacture Cake
|__ Make Cake
| |__ Make Batter
| | |__ Add Dry Ingredients
| | |__ Add Liquids
| | |__ Mix
| |__ Fill Pan
| |__ Bake
| |__ Frost
|
|__ Package Cake
|__ Box
|__ Label

In the Composite pattern, we’ll model every step in a separate class with a common interface, which will report back how long they take. So we’ll define a common base class, Task, which plays the role of component.

class Task
attr_accessor :name, :parent
def initialize(name)
@name = name
@parent = nil
end

def get_time_required
0.0
end
end

We can now create the classes in charge of the most basic jobs, this is, leaf classes, like AddDryIngredientsTask:

class AddDryIngredientsTask < Task
def initialize
super('Add dry ingredients')
end
def get_time_required
1.0
end
end

What we need now is a container to deal with complex tasks, which are internally built up of any number of subtasks, but from the outside look like any other Task. We’ll create the composite class:

class CompositeTask < Task
def initialize(name)
super(name)
@sub_tasks = []
end
def add_sub_task(task)
@sub_tasks << task
task.parent = self
end
def remove_sub_task(task)
@sub_tasks.delete(task)
task.parent = nil
end

def get_time_required
@sub_tasks.inject(0.0) {|time, task| time += task.get_time_required}
end
end

With this base class we can build complex tasks that behave like a simple one, as it implements the Task interface, and also add subtasks with the method add_sub_task. We’ll create the MakeBatterTask

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

We must keep in mind that the objects tree may go as deep as we want. MakeBatterTask contains only leaf objects, but we could create a class that contains composite objects and it would behave exactly the same:

class MakeCakeTask < CompositeTask
def initialize
super('Make cake')
add_sub_task(MakeBatterTask.new)
add_sub_task(FillPanTask.new)
add_sub_task(BakeTask.new)
add_sub_task(FrostTask.new)
add_sub_task(LickSpoonTask.new)
end
end

--

--

Swati Panhalkar

If there is one substance by which everything is held together, it is love.