Main page background image

Ruby Composite Design Pattern


Ihor T.
RoR Developer

Composite Design Pattern

The composite pattern states that we create larger objects from smaller objects, which themselves can be made up of even smaller objects.

To build a composite pattern:

  • we need to create an abstract class or base class, sometimes called the Component class, which implements a common interface for Composite and Leaf objects.
  • we need one or more Leaf classes, which are simple separate blocks of the process.
  • we need a high-level class, sometimes called a Composite class. A composite class is built from components or other composites.

To understand the pattern, let’s create the most useless program ever.

Composite Design Pattern example

Imagine a program that represents a book that has many paragraphs and paragraphs are made up of sentences and sentences are made up of words.

Main parts of the program:

Component is our base class that implements everything we have in common for all objects. In our case, in common between books, paragraphs and sentences.

Composite is the main implementation of a composite object, in our case, is responsible for adding or removing child objects and implements common operations for all composite objects.

Book and Paragraph are also composite objects, which can add something specific to each composite object. In our case, these are empty classes. I’ll leave it up to your imagination what we can add there.

Sentence is a Leaf object. Just like Composite, it implements the Component interface, but the difference between them is that Leaf does not have child elements, and the functionality to add/remove nested elements is not available there.

# Component
# Includes what we have in common in high-level objects (Composite) and low-level objects (Leaf).
class Component
  attr_accessor :name, :parent

  def initialize(name)
    @name = name
    @parent = nil
  end

  def add
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  def delete
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# Composite
# A high-level component responsible for the main task of the program
class Composite < Component
  def initialize(name)
    super(name)
    @child_elements = []
  end

  def add(element)
    @child_elements << element
    element.parent = self
  end

  def delete(element)
    @child_elements.delete(element)
    element.parent = self
  end

  def sentences_count
    @child_elements.size
  end
end

# Composite
class Book < Composite
  def sentences_count
    @child_elements.map(&:sentences_count).inject(:+)
  end
end

# Composite
class Paragraph < Composite
  # You can add any logic you want to control an individual paragraph.
end

# Leaf
# Responsible for basic, simple work
# Must implement component interface
class Sentence < Component
  # You can add any logic you want to control an individual sentence.
end

# creates two sentences
sentence_1 = Sentence.new('Sentence 1')
sentence_2 = Sentence.new('Sentence 2')

# creates one paragraph
paragraph = Paragraph.new('Paragraph 1')

# adds two sentences to the paragraph
paragraph.add(sentence_1)
paragraph.add(sentence_2)

# creates one book
book = Book.new('Book 1')

# adds one paragraph to the book
book.add(paragraph)

# amount of the sentences in the paragraph
paragraph.sentences_count # => 2

# amount of the sentences in the book
book.sentences_count # => 2

# finds which paragraph the sentence belongs to
sentence_1.parent.name # => Paragraph 1

The only work our program can do is to count the number of sentences for each paragraph or for the entire book. In addition, we can check which composite each component belongs to.

Summary

That’s it, my friends. We discovered one of the GoF patterns - Composite Design Pattern.