[Talking-Ruby] Forwardable module provides delegation of specified methods to a designated object

Forwardable

The Forwardable module provides delegation of specified methods to a designated object, using the methods def_delegator and def_delegators.

forwardable.rb provides single-method delegation via the #def_delegator and #def_delegators methods. For full-class delegation via DelegateClass, see delegate.rb.

For example, say you have a class RecordCollection which contains an array @records. You could provide the lookup method record_number(), which simply calls [] on the @records array, like this:

1
2
3
4
5
6
7
require 'forwardable'

class RecordCollection
attr_accessor :records
extend Forwardable
def_delegator :@records, :[], :record_number
end

We can use the lookup method like so:

1
2
3
r = RecordCollection.new
r.records = [4,5,6]
r.record_number(0) # => 4

Further, if you wish to provide the methods size, <<, and map, all of which delegate to @records, this is how you can do it:

1
2
3
4
5
6
7
8
9
10
class RecordCollection # re-open RecordCollection class
def_delegators :@records, :size, :<<, :map
end

r = RecordCollection.new
r.records = [1,2,3]
r.record_number(0) # => 1
r.size # => 3
r << 4 # => [1, 2, 3, 4]
r.map { |x| x * 2 } # => [2, 4, 6, 8]

You can even extend regular objects with Forwardable.

1
2
3
4
my_hash = Hash.new
my_hash.extend Forwardable # prepare object for delegation
my_hash.def_delegator "STDOUT", "puts" # add delegation for STDOUT.puts()
my_hash.puts "Howdy!"

Another example

You could use Forwardable as an alternative to inheritance, when you don’t want to inherit all methods from the superclass. For instance, here is how you might add a range of Array instance methods to a new class Queue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Queue
extend Forwardable

def initialize
@q = [ ] # prepare delegate object
end

# setup preferred interface, enq() and deq()...
def_delegator :@q, :push, :enq
def_delegator :@q, :shift, :deq

# support some general Array methods that fit Queues well
def_delegators :@q, :clear, :first, :push, :shift, :size
end

q = Queue.new
q.enq 1, 2, 3, 4, 5
q.push 6

q.shift # => 1
while q.size > 0
puts q.deq
end

q.enq "Ruby", "Perl", "Python"
puts q.first
q.clear
puts q.first

This should output:

1
2
3
4
5
6
7
2
3
4
5
6
Ruby
nil

More examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyQueue
CONST = 1
extend Forwardable
attr_reader :queue
def initialize
@queue = []
end

def_delegator :@queue, :push, :mypush
def_delegator 'MyQueue::CONST', :to_i
end

q = MyQueue.new
q.mypush 42
q.queue #=> [42]
q.push 23 #=> NoMethodError
q.to_i #=> 1

Be advised, RDoc will not detect delegated methods.


References

[1] Module: Forwardable (Ruby 3.0.0) - https://ruby-doc.org/stdlib-3.0.0/libdoc/forwardable/rdoc/Forwardable.html

[2] Ruby’s Hidden Gems: Delegator and Forwardable | AppSignal Blog - https://blog.appsignal.com/2019/04/30/ruby-magic-hidden-gems-delegator-forwardable.html

[3] class SimpleDelegator - Documentation for Ruby 3.0.0 - https://docs.ruby-lang.org/en/3.0.0/SimpleDelegator.html

[4] Module | Ruby on Rails 6.1.4 Class - https://api.rubyonrails.org/classes/Module.html#method-i-delegate

[5] saturnflyer/casting: Delegate methods in Ruby and preserve self. Add behaviors to your objects without altering their superclass hierarchy. - https://github.com/saturnflyer/casting

[6] How To Delegate Methods in Ruby & Ruby on Rails - RubyGuides - https://www.rubyguides.com/2018/10/delegate-methods-in-ruby/