The Composite Pattern
July 16, 2019
This is a post about the Composite Pattern and is from my study of design patterns
Problem borrowed from here
Let's say that I am building inventory tracking software for a company that has to package and ship complex orders of products. An order is a box. A box can contain other boxes or products. I need a way to get the total weight and inventory count of any given box. The composite pattern let's me strucutre all of this as a tree. Both the Box
and Product
type respond to the weight
and item_count
method. Products know their own weight and that they have an item count of 1 (duh). Boxes ask all of their direct children their weight and item_count and return the sum. Recursion!
First, here's how it would look without the composite pattern. Bleh!
require './v1/box'
require './v1/product'
b1 = Box.new
hammer_box = Box.new
hammer = Product.new 'hammer', 10.0
hammer_box.add_item hammer
b1.add_item hammer_box
recipt = Product.new 'recipt', 0
b1.add_item recipt
other_box = Box.new
phone_box = Box.new
phone = Product.new 'phone', 1.0
headphones = Product.new 'headpones', 0.1
phone_box.add_item phone
phone_box.add_item headphones
charger_box = Box.new
charger = Product.new 'charger', 0.2
charger_box.add_item charger
other_box.add_item phone_box
other_box.add_item charger_box
b1.add_item other_box
puts "shipment: #{b1.item_count} item(s), #{b1.weight} lbs"
The composite pattern is convenient because we have a common interface for getting at item's (either product or collection of products) weight and count. Think of it another way, if I want to know the weight of an item, I shouldn't have to be concerned with dumping out its entire contents and computing it myself. Instead just pass the weight
message and let my objects do their thing.
The above code gets the job done, but its awfully verbose. Defining a product on one line and then telling it what box it goes in is tedious. Instead lets use the builder pattern and blocks to specify how we want things structured.
require './v2/shipment'
s = Shipment.new do
box do
product 'hammer', 10.0
end
box do
box do
product 'phone', 1.0
product 'headphones', 0.1
end
box do
product 'charger', 0.2
end
end
product 'recipt', 0
end
puts "shipment: #{s.item_count} item(s), #{s.weight} lbs"
Ah, much better! Now we have a Shipment
instance which represents all of the items in a shipment. Shipment
has two public methods box
and product
to let us define those items
def box(&block)
box = Box.new
parent.pack box
add_node(box, &block)
end
def product(name, weight)
product = Product.new(name, weight)
parent.pack product
add_node(product)
end
Both of those methods use a method called add_node
to add keep track of the item
# adds a node to the shipment tree.
# node: a node like object (Box or Product)
def add_node(node, &block)
# apppend node to list of nodes
# if no block given return
# push node to the stack
# eval node's block
# -> this will lead to some recursion
# when we resurface (all children have been delt with), pop node from the stack
@nodes << node
return unless block_given?
@nodes_stack << node
instance_eval(&block)
@nodes_stack.pop
end
Basically we are recursively evaling blocks until we get to a leaf node (product). It uses a stack to keep track of previous items added. At any given time when we are adding an item, its parent is the last item on the stack. If the stack is empty the item's parent is the top level box (stored as @root_box
). Shipment handles creating an item, adding it to the stack and loading it into its parent box. The win here is that we've now created an interface that lets us simply express the structure of a shipment, and not worry about the messy details of loading products and boxes in to their parent.