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
andSimpleDelegator
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.