The Composite Pattern

What?

Once applied, the Composite Pattern allows you to operate on objects of different types within a tree structure using a single interface.

         Folder
        /      \
   Folder      Folder
  /     \      /    \
File   File  File   File

Why?

By using a single interface to operate on objects of different types, you can simplify your client code by making it type agnostic.

In the above diagram, you might wish to ask any object its size without worrying about whether that size is directly returned from a File object or recursively calculated from the contents of a folder.

How?

Firstly, we need to define a common interface that will be implemented by all members of the tree structure, whether they are primitives (Files) or containers (folders). Ideally, this interface should implement as many of the common operations as possible, minimising the amount of behaviour that will need to be implemented by subclasses.

class Component
  def size
    raise NotImplementedError.new, "#size is not implemented on #{self.class}"
  end

  def children
    []
  end

  def add(child); end

  def remove(child); end

  def get_child(index)
    nil
  end
end

We can then implement the behaviour in our composite Folder class. Specifically, we implement the behaviour responsible for operating on the list of the object's direct children, as well as the behaviour that delegates the calculation of the folder's size to that list of children.

class Folder
  def initialize
    @children = []
  end

  def size
    children.collect(&:size).sum
  end

  def children
    @children
  end

  def add(child)
    @children << child
  end

  def remove(child)
    @children.delete(child)
  end

  def get_child(index)
    children[index]
  end
end

Finally, we implement the operation on our leaf File class.

class File
  def size
    size_on_disk
  end
end

Our client might then use the code like:

documents = Folder.new
documents.add(File.new)
documents.add(File.new)

baking_recipes = Folder.new
baking_recipes.add(File.new)

vegetable_recipes = Folder.new
vegetable_recipes.add(File.new)
vegetable_recipes.add(File.new)

all_recipes = Folder.new
all_recipes.add(baking_recipes)
all_recipes.add(vegetable_recipes)

file_system = Folder.new
file_system.add(documents)
file_system.add(all_recipes)

# This creates the following structure:
#
# file_system
# |- documents
#    |- file
#    |- file
# |- all_recipes
#    |- baking_recipes
#    |  |- file
#    |- vegetable_recipes
#       |- file
#       |- file

Our client can now call size on any object in the above structure without regard for the object's type.