Search code examples
rubydelegation

Delegate attribute methods to the parent object


I have the following class:

class Alphabet

  attr_reader :letter_freqs, :statistic_letter

  def initialize(lang)
    @lang = lang
    case lang
    when :en
      @alphabet = ('A'..'Z').to_a
      @letter_freqs = { ... }
    when :ru
      @alphabet = ('А'..'Я').to_a.insert(6, 'Ё')
      @letter_freqs = { ... }
    ...
    end
    @statistic_letter = @letter_freqs.max_by { |k, v| v }[0]
  end

end

foo = Alphabet.new(:en)

The central member here is @alphabet.

I'd like to make it some sort of a container class to invoke Array methods directly like

foo[i]
foo.include?

instead of explicitly accessing @alphabet:

foo.alphabet[i]
foo.alphabet.include?  

I know I could define a lot of methods like

def [](i)
  @alphabet[i]
end

but I'm looking for a proper way of "inheriting" them.


Solution

  • You can use Forwardable (it is included in the Ruby standard library):

    require 'forwardable'
    
    class Alphabet
    
      extend Forwardable
      def_delegators :@alphabet, :[], :include?
    
      def initialize
        @alphabet = ('A'..'Z').to_a
      end
    
    end
    
    foo = Alphabet.new
    
    p foo[0]           #=> "A"
    p foo.include? 'ç' #=> false
    

    If you wish to delegate all the methods not defined by your class you can use SimpleDelegator (also in the standard library); it lets you delegate all the methods that are not responded by the instance to an object specified by __setobj__:

    require 'delegate'
    
    class Alphabet < SimpleDelegator
    
      def initialize
        @alphabet = ('A'..'Z').to_a
        __setobj__(@alphabet)
      end
    
      def index
        'This is not @alphabet.index'
      end
    
    end
    
    foo = Alphabet.new
    
    p foo[0]           #=> "A"
    p foo.include? 'ç' #=> false
    p foo.index        #=> "This is not @alphabet.index"
    

    When the delegate doesn't need to be dynamic you can arrange the master class to be a subclass of DelegateClass, passing the name of the class to be delegated as argument and calling super passing the object to be delegated in the #initialize method of the master class:

    class Alphabet < DelegateClass(Array)
    
      def initialize
        @alphabet = ('A'..'Z').to_a
        super(@alphabet)
      end
    

    More info about the delegation design pattern in Ruby here