Search code examples
rubydelegation

Specify class with forwardable


I'm trying to use ruby's forwardable module to make some variables in one class accessible to another class. However I am having some trouble doing this.

It seems that I'm able to 'forward' some variables within the self (first bit of code) but I'm unable to forward some variable within a class (second bit of code)

The following works:

require 'forwardable'
module ModuleName
  #
  class << self
    attr_accessor :config
    def run
      @config = {hey: 'hi', jay: 'ji'}
      puts "1) Config = #{config}"
    end
  end

  #
  class Start
    extend Forwardable
    def_delegators ModuleName, :config
    def run
      puts "2) Config = #{config}"
    end
  end
end


ModuleName.run
(ModuleName::Start.new).run

#=> 1) Config = {:hey=>"hi", :jay=>"ji"}
#=> 2) Config = {:hey=>"hi", :jay=>"ji"}

BUT this doesn't

require 'forwardable'
module ModuleName
  #
  class Data
    attr_accessor :config
    def run
      @config = {hey: 'hi', jay: 'ji'}
      puts "1) Config = #{config}"
    end
  end

  #
  class Start
    extend Forwardable
    def_delegators ModuleName::Data, :config
    def run
      puts "2) Config = #{config}"
    end
  end
end


(ModuleName::Data.new).run
(ModuleName::Start.new).run

#=> 1) Config = {:hey=>"hi", :jay=>"ji"}
#=> /Users/ismailm/Desktop/ex.rb:17:in `run': undefined method `config' for ModuleName::Data:Class (NoMethodError)

Can you help in fixing this part of the code...


Solution

  • Typically when you delegate between two classes, one object contains an instance of the other. The fact that you call (ModuleName::Data.new).run implies to me that is what you are trying to do, but are somehow missing the fact that you need an instance of the contained class to be stored somewhere in order to receive the call to :config

    This variation of your second piece of code is closer to what I would expect to see in a delegation scenario:

    require 'forwardable'
    module ModuleName
      #
      class Data
        attr_accessor :config
    
        def initialize
          @config = {hey: 'hi', jay: 'ji'}
        end
    
        def run
          puts "1) Config = #{config}"
        end
      end
    
      #
      class Start
        extend Forwardable
    
        def initialize data_obj = Data.new()
          @data = data_obj
        end
    
        def_delegators :@data, :config
    
        def run
          puts "2) Config = #{config}"
        end
      end
    end
    
    
    (ModuleName::Data.new).run
    (ModuleName::Start.new).run
    

    I changed the constructor to ModuleName::Start in order to show a common pattern, not used here. Namely, you often pass in the wrapped object, or even more commonly the params that allow you to construct a new one and assign it to the instance variable that you wish to delegate to.

    A minor related change: In your original code, the value of @config was only set via calling :run, so delegating direct to :config won't read the value you expect in the test. It worked in the first version because @config was set in the first call to run on the module, and then read globally as a singleton method on the second delegated call. I have worked around that here by setting it in Datas constructor, but of course anything that sets the value of @config on the instance you are delegating to would work.