Search code examples
rubyoopdelegation

Delegation in Ruby


I have a class Klass, and its constructor accepts an argument. We should be able to call methods on this object that are not defined in Klass.

We can chain multiple methods, but in the end, we have to use Klass#result to get the result like:

Klass.new(5).pred.pred.result

and the output here should be 3. I tried using method_missing in Klass and using send on the object's class, but that would have worked without the result method that I have to use. Can someone explain how this can be done with delegation?


Solution

  • You could do something like this:

    class Klass
      def initialize(number)
        @number = number
      end
    
      def result
        @number
      end
    
      def method_missing(method_name, *arguments, &block)
        if @number.respond_to?(method_name)
          @number = @number.method(method_name).call(*arguments, &block)
          return self
        else
          super
        end
      end
    
      def respond_to_missing?(method_name, include_private = false)
        # be sure to implement this...
      end
    end
    
    puts Klass.new(5).pred.pred.result # => 3
    

    But it's problematic. In this particular example, since #pred returns a new object (it doesn't modify the object it was called on), we have to reassign the instance variable to the result. It works for pred and other methods that return new Integers, but some methods on Integer don't return an Integer (e.g. Integer#even). In this case you'd get this sort of behavior:

    puts Klass.new(4).even?.result # => true
    

    Depending on your particular situation, that might be what you're after. Or, it might be that in your situation all methods the object being delegated to mutate that object, rather than return new instances of the object, in which case the reassignment isn't needed.

    I don't think you can use Ruby's existing Delegator and SimpleDelegator constructs, because the only way you can chain the final #result call onto the end is if every delegated call returns the instance of Klass. Using those existing constructs would cause delegated calls to return their normal return values, and the chaining would then be on whatever objects those return values return. For example, using the above code, you'd see this behavior:

    puts Klass.new(5).pred.pred.class # => "Klass"
    

    Using SimpleDelegator, you'd see this behavior

    require 'delegate'
    
    class Klass2 < SimpleDelegator
      # Klass2 methods...
    end
    
    puts Klass2.new(5).pred.pred.class # => "Fixnum"
    

    Hope that helps.