Multiple Delegates in Ruby

I found that, out of the box, delegating method calls to multiple objects is not straight-forward in Ruby. There are two modules in the Ruby standard library that cover most delegation use cases: The delegate module lets you easily delegate all methods to another object, and the forwardable module does the same for some explicitly enumerated methods. Both of them operate on a single delegate.

There are good reasons for this. With multiple delegates it’s not exactly obvious what to do with the multiple return values, and the side effects when a delegate mutates method arguments could be a real headache.

I had a use case where these issues didn’t matter. I wanted to capture $stdout but not actually hide anything from it, meaning that I wanted all output to be written to the console (or whatever $stdout amounts to in any given situation) while creating an in-memory copy of it on the side. My idea was to replace $stdout with an object that delegates both to the original $stdout and to a StringIO object.

I came up with this implementation of a MultiDelegator:

require 'delegate'

class MultiDelegator

  def initialize(delegates)
    @delegates = delegates.map do |del|
      SimpleDelegator.new(del)
    end
  end

  def method_missing(m, *args, &block)
    return_values = @delegates.map do |del|
      del.method_missing(m, *args, &block)
    end
    return_values.first
  end
end

This will forward all method calls to the all the delegates in the list passed to the constructor. Arbitrarily but consistently, all return values except for the one from the first delegate are discarded.

I’m wrapping each delegate in a SimpleDelegator (from the builtin delegate module) because I’m lazy and don’t want to copy the code that deals with the actual method forwarding from there (it’s pretty simple though, you can see for yourself).

This implementation is somewhat simplistic and comes with caveats:

  • It bears repeating: If delegated methods mutate their arguments, you’re gonna have a bad time, most likely.
  • This obviously does not support some of the nice-to-have functionality that the builtin Delegator and SimpleDelegator handle well, such as object equality, freezing, marshaling, tainting.

As a usage example, this is how I solved my original output capturing problem:

captured_output = StringIO.new
begin
  $stdout = MultiDelegator.new([$stdout, captured_output])
  # do something that generates output
  # ...
ensure
  $stdout = STDOUT
end
# do something with captured_output
# ...

Side note: I’m aware that replacing the global $stdout will not actually capture everything that you would consider output. It won’t capture output generated by C extensions or subprocesses; see this discussion for more details.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s