Ruby Design Pattern: Composite Method
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.
- A common interface or base class for all of your objects
- One or more leaf classes
- 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