Coffee Coffee
If you’re anything like me, you’ll agree that every morning needs to start out with a cup of coffee. And, if you’re anything like me, you’ll have at least three different coffee making apparatuses. And, if you’re anything like me… you’ll soon realize you may have an addiction.
Joke aside, each coffee contraption requires a specific procedure to be completed in order to brew a cup of joe; each having multiple parts, taking differing amounts of time, requiring various numbers of steps, etc.
Our coffee making process can be described by a basic example of the Composite method pattern.
The Best Part of Waking Up is a Composite Pattern in Your Cup
We can start by thinking of each coffee maker and coffee related task as a subclass of
our CoffeeRoutine
. CoffeeRoutine
will be known as the component, the base
class or interface that possesses the commonalities of simple and complex
objects. CoffeeRoutine#time
is the common trait among all
coffee related classes.
class CoffeeRoutine
attr_reader :task
def initialize(task)
@task = task
end
def time
0.0
end
end
Next, we’ll create a couple of leaf classes, which represent
indivisble portions of our pattern. Here are a couple of leaf classes
that come to mind: GrindCoffee
and BoilWater
. These leaf classes are
our most basic steps to making coffee.
class GrindCoffee < CoffeeRoutine
def initialize
super 'Grinding some coffee!'
end
def time
0.5
end
end
class BoilWater < CoffeeRoutine
def initialize
super 'Boiling some water!'
end
def time
4.0
end
end
class AddCoffee < CoffeeRoutine
def initialize
super 'Adding in the coffee!'
end
def time
1.0
end
end
g = GrindCoffee.new
g.task # => 'Grinding some coffee!'
g.time # => 0.5
Now, we can get to the namesake of the pattern: the composite class. A composite class is a component that also contain subcomponents. Composite classes can be made up of smaller composite classes or leaf classes.
Our various coffee making apparatuses can be thought of as composites.
Let’s check out the FrenchPress
class:
class FrenchPress < CoffeeRoutine
attr_reader :task, :steps
def initialize(task)
super 'Using the French press to make coffee'
@steps = []
add_step BoilWater.new
add_step GrindCoffee.new
add_step AddCoffee.new
end
def add_step(step)
steps << step
end
def remove_step(step)
steps.delete step
end
def time_required
total_time = 0.0
steps.each { |step| total_time += step.time }
total_time
end
end
However, we can simplify the FrenchPress
class by pulling out the
composite functionality into its own class.
class CompositeTasks < CoffeeRoutine
attr_reader :task, :steps
def initialize(task)
@steps = []
end
def add_step(step)
steps << step
end
def remove_step(step)
steps.delete step
end
def time_required
total_time = 0.0
steps.each { |step| total_time += step.time }
total_time
end
end
Now we can create composite coffee makers easily… They’ll look something like this:
class FrenchPress < CompositeTasks
def initialize
super 'Using the FrenchPress to make coffee!!!'
add_step GrindCoffee.new
add_step BoilWater.new
add_step AddCoffee.new
# ... Omitted actual steps to make coffee from a French press ...
# ... Imagine PressPlunger class has been defined already ...
add_step PressPlunger.new
end
end
class DripMaker < CompositeTasks
def initialize
super 'Using the DripMaker to make coffee!!!'
add_step GrindCoffee.new
add_step BoilWater
add_step AddCoffee.new
# ... Imagine PressStartButton class has been defined already ...
add_step PressStartButton.new
end
end
Swell… now we can call the FrenchPress
and DripMaker
coffee makers.
frenchpress = FrenchPress.new
# => #<FrenchPress:0x007f88fcf46410
@task="Using the FrenchPress to make coffee!!!",
@steps=
[#<GrindCoffee:0x007f88fcf46370 @step="Grinding some coffee!">,
#<BoilWater:0x007f88fcf46320 @step="Boiling some water!">]>
#<AddCoffee:0x007f88fcf46329 @step="Adding in the coffee!">]>
#<PressPlunger:0x007f88fcf46098 @step="Pressing the plunger down!">]>
dripmaker = DripMaker.new
# => #<DripMaker:0x137t88fcf57109
@task="Using the DripMaker to make coffee!!!",
@steps=
[#<GrindCoffee:0x007f88fcf46370 @step="Grinding some coffee!">,
#<BoilWater:0x007f88fcf52520 @step="Boiling some water!">]>
#<AddCoffee:0x007f88fcf46123 @step="Adding in the coffee!">]>
#<PressStartButton:0x007f88fcf46432 @step="Pushing the start button!">]>
Now we can also check the time required for each coffee maker.
frenchpress.time_required # => 12.4
dripmaker.time_required # => 8.5
Discussion
Implementing the Composite pattern is pretty simple.
We create a component class that ties the numerous simple and
complex characteristics together. In our example, CoffeeRoutine
defines an elementary method #time
and each child class implements
its own amount.
Next, we create leaf classes, AddCoffee
, BoilWater
, and GrindCoffee
,
that share the same characteristics with one another. Remember that it’s the nature
of leaf classes to be simple. If you happen across a leaf class that
could be broken up, it might potentially be a composite class in disguise.
Break up those actions into individual leaf classes and turn the original class
into a composite. All of our leaf classes had a #time
method.
The composite class handles all the subtasks, essentially using the child classes
at its will. We can see that our two composite classes and their methods, FrenchPress#time_required
and DripMaker#time_required
. manipulate the method #time
from the leaf classes.
Ultimately, our coffee makers are able to treat each step,
GrindCoffee
, BoilWater
and AddCoffee
uniformly.
Hope this helps you with your morning routine!