Composite

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

Participants

RoleIn this exampleResponsibility
Component#linesThe interface shared by single widgets and groups.
LeafLabelA widget with no children. It produces the actual text.
CompositeGroupHolds widgets (labels or more groups) and combines their results.
ClientCaller ofDoesn't need to know whether it holds one label or the whole sidebar.

Ruby implementation

composite.rb
# The Leaf: one line of text in the sidebar.
class Label
  def initialize(text)
    @text = text
  end

  def lines = [@text]
end

# The Composite: a group of widgets, any of which may itself be a group.
# It answers the same messages as a Label, so the UI treats both alike.
class Group
  include Enumerable

  def initialize(*widgets)
    @widgets = widgets
  end

  def add(widget)
    @widgets << widget
    self
  end

  def each(&block)
    @widgets.each(&block)
  end

  def lines
    flat_map(&:lines)
  end

  def height
    lines.size
  end
end

In use

towers  = Group.new(Label.new("Towers"), Label.new("1 Arrow  50g"), Label.new("2 Frost  70g"))
sidebar = Group.new(Label.new("Gold: 120"), Label.new("Lives: 20"), towers)

sidebar.lines
# => ["Gold: 120", "Lives: 20", "Towers", "1 Arrow  50g", "2 Frost  70g"]

sidebar.height # => 5
towers.height  # => 3

Notes

Ruby idioms

  • include Enumerable and define #each. The composite then gets flat_map, select, count and the rest for free. #lines becomes flat_map(&:lines).
  • Let the leaf answer the group’s question too. Label#lines returns a one-element array, so flat_map(&:lines) works however deep the tree goes, with no type checks.

Reach for it when

  • Your data is a tree: UI widgets, menus, files and folders, an organisation chart.
  • Clients would otherwise be full of if widget.is_a?(Group) checks.

Watch for

  • Uniformity can hide meaningful differences. If #add makes no sense on a leaf, leave it off the leaf. Don’t add a method that raises just to keep the interfaces identical.

One label or the whole sidebar: both answer the same question.