Menu
Coddy logo textTech

Builder Pattern

Part of the Object Oriented Programming section of Coddy's Ruby journey. Lesson 48 of 57.

The builder pattern creates a complex object step by step. Each step is a method that stores one choice and returns the builder, so the steps chain; build checks the choices and returns the finished object:

class Pizza
  def initialize(size, toppings)
    @size = size
    @toppings = toppings
  end

  def to_s
    "#{@size} pizza with #{@toppings.join(", ")}"
  end
end

class PizzaBuilder
  def initialize
    @toppings = []
  end

  def size(value)
    @size = value
    self
  end

  def topping(name)
    @toppings << name
    self
  end

  def build
    raise ArgumentError, "size is required" if @size.nil?

    Pizza.new(@size, @toppings)
  end
end

puts PizzaBuilder.new.size("large").topping("cheese").topping("ham").build

Output:

large pizza with cheese, ham

Because the builder only creates the object in build, it can check the whole combination at once, such as a required part or two options that cannot go together:

class TicketBuilder
  def seat(value)
    @seat = value
    self
  end

  def standing
    @standing = true
    self
  end

  def build
    raise ArgumentError, "seat and standing exclude each other" if @seat && @standing

    @standing ? "standing ticket" : "ticket for seat #{@seat || "any"}"
  end
end

puts TicketBuilder.new.seat("B7").build
puts TicketBuilder.new.standing.build
begin
  TicketBuilder.new.seat("A1").standing.build
rescue ArgumentError => e
  puts e.message
end

Output:

ticket for seat B7
standing ticket
seat and standing exclude each other

A common Ruby form hands the builder to a block. The class method creates the builder, yields it and calls build at the end:

class Email
  attr_accessor :to, :subject, :lines

  def self.compose
    email = new
    email.lines = []
    yield email
    email
  end

  def to_s
    "To: #{to}\nSubject: #{subject}\n\n#{lines.join("\n")}"
  end
end

mail = Email.compose do |e|
  e.to = "ada@x.io"
  e.subject = "Hi"
  e.lines << "Hello Ada,"
  e.lines << "see you soon."
end
puts mail

Output:

To: ada@x.io
Subject: Hi

Hello Ada,
see you soon.

For a handful of optional values, Ruby's keyword arguments with defaults are often enough. A builder pays off when parts can repeat, order matters, or the combination needs checking before the object exists:

Pizza.new(size: "large", crust: "thin")          # few options: keywords

Query.new.from("users").where("age > 18").where("active = 1").limit(10).to_s
challenge icon

Challenge

Easy

Complete PizzaBuilder and Pizza. The builder's steps size(value), crust(value) and topping(name) (repeatable) return the builder. build raises ArgumentError with size is required when no size was given, uses the crust classic when none was chosen, and returns a Pizza. Pizza#to_s returns large thin pizza with cheese, ham, or small classic pizza with no toppings.

Each input line is one order such as size=large crust=thin topping=cheese topping=ham. The supplied code sends each part to the builder in order, by chaining, and prints the pizza or the error message.

Your code goes in pizza.rb and pizza_builder.rb. main.rb holds the supplied input/output code and cannot be edited.

Try it yourself

require_relative 'pizza'
require_relative 'pizza_builder'

# Supplied input/output code: keep it as it is
input = []
while (line = gets)
  input << line.chomp
end
input.each do |line|
  builder = line.split.reduce(PizzaBuilder.new) do |b, part|
    step, value = part.split("=")
    b.public_send(step, value)
  end
  begin
    puts builder.build
  rescue ArgumentError => e
    puts e.message
  end
end
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Online Ruby compiler