Menu
Coddy logo textTech

Delegation with Forwardable

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

Delegation means an object hands some of its work to an object it holds, instead of inheriting from it. A playlist that has an array of songs is safer than one that is an array, because outside code only gets the methods you choose to forward:

class Playlist
  def initialize
    @songs = []
  end

  def add(song)
    @songs << song
    self
  end

  def size
    @songs.size
  end

  def first
    @songs.first
  end
end

list = Playlist.new.add("Intro").add("Dune")
puts list.size
puts list.first

Output:

2
Intro

Writing each forwarding method by hand is repetitive. The standard library's Forwardable does it: extend Forwardable, then def_delegators :@songs, :size, :first forwards those calls to the object in @songs:

require 'forwardable'

class Playlist
  extend Forwardable
  def_delegators :@songs, :size, :first, :last, :empty?, :include?

  def initialize
    @songs = []
  end

  def add(song)
    @songs << song
    self
  end
end

list = Playlist.new.add("Intro").add("Dune")
puts "#{list.size} #{list.first} #{list.last}"
puts list.include?("Dune")
puts list.empty?

Output:

2 Intro Dune
true
false

def_delegator :@songs, :push, :add forwards one method under a new name. The object keeps full control of everything it does not forward, for example delete or clear:

require 'forwardable'

class Queue2
  extend Forwardable
  def_delegator :@jobs, :push, :enqueue
  def_delegator :@jobs, :shift, :next_job
  def_delegators :@jobs, :size

  def initialize
    @jobs = []
  end
end

q = Queue2.new
q.enqueue("a")
q.enqueue("b")
puts q.next_job
puts q.size
puts q.respond_to?(:clear)

Output:

a
1
false

Prefer delegation ("has a") when a class needs only part of another class's behaviour. Inheriting from Array would expose dozens of methods, such as clear and delete_at, that could break the playlist's rules:

class Playlist < Array      # every Array method is public, including clear
end

class Playlist              # only what you forward is public
  extend Forwardable
  def_delegators :@songs, :size, :each
end
challenge icon

Challenge

Easy

Complete the class Playlist with Forwardable. It holds songs, each a [title, seconds] pair, in @songs. Forward size, empty? and first with def_delegators, and forward push under the name add with def_delegator. Its own method duration returns the total length as m:ss, and titles returns the titles joined by , .

The supplied code adds a song per input line (title,seconds) with add, then prints the size, the first title, the titles, the duration and whether the playlist responds to clear.

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

Try it yourself

require_relative 'playlist'

# Supplied input/output code: keep it as it is
input = []
while (line = gets)
  input << line.chomp
end
list = Playlist.new
input.each do |line|
  title, seconds = line.split(",")
  list.add([title, seconds.to_i])
end
puts "songs: #{list.size}"
puts "first: #{list.empty? ? "none" : list.first[0]}"
puts "titles: #{list.titles}"
puts "duration: #{list.duration}"
puts "can clear: #{list.respond_to?(:clear)}"
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