How do you access instance variables from a Class object across different Classes within the same namespace in Ruby

67 Views Asked by At

Let's say I am working on a problem where a class object will only be instantiated once, such as a Gem configuration object. The configuration class calls many classes that use the configuration class object's instance variables.

How do you access the configuration object instance variables in these other classes? Here are some example implementations (passing the config object as self versus storing the config object as a class instance variable on the Module) that hopefully help clarify:

# This example passes the Config object as self
module NumberTally
  class Config
    attr_reader :to_tally

    def initialize
      @to_tally = [1,2,3,4,5]
      perform
    end

    private

    def perform
      validate_to_tally
      output_to_tally
    end

    def validate_to_tally
      ValidateToTally.new(self).call
    end

    def output_to_tally
      OutputToTally.new(self).call
    end
  end

  class ValidateToTally
    attr_reader :to_tally

    def initialize(config)
      @to_tally = config.to_tally
    end

    def call
      raise TypeError if to_tally.any?{|num| num.class != Integer }
    end
  end

  class OutputToTally
    attr_reader :to_tally

    def initialize(config)
      @to_tally = config.to_tally
    end

    def call
      to_tally.each do |num|
        puts num
      end
    end
  end
end


#use Class instance variable in the Module
module NumberTally
  class << self
    attr_accessor :config
  end

  class Config
    attr_reader :to_tally

    def initialize
      @to_tally = [1,2,3,4,5]
      NumberTally.config = self
      perform
    end

    private

    def perform
      validate_to_tally
      output_to_tally
    end

    def validate_to_tally
      ValidateToTally.new.call
    end

    def output_to_tally
      OutputToTally.new.call
    end
  end

  class ValidateToTally
    def call
      raise TypeError if to_tally.any?{|num| num.class != Integer }
    end

    private

    def to_tally
      @to_tally ||= NumberTally.config.to_tally
    end
  end

  class OutputToTally
    def call
      to_tally.each do |num|
        puts num
      end
    end

    private

    def to_tally
      @to_tally ||= NumberTally.config.to_tally
    end
  end
end

2

There are 2 best solutions below

2
Schwern On

If I understand, you want a single Config object that everything in the code-base uses. We can make something like Rails.configuration.

Make a Config object. Make class to encompass your code (like the Rails class) to hold defaults such as the configuration. Give it a class method to instantiate and offer the default config object.

class App
  class Config
    attr_accessor :numbers
  end

  class << self
    attr_writer :config
    def config
      @config ||= make_config
    end

    private def make_config
      # Do any app-specific configuration here, such as reading a config file
      # and setting defaults.
      config = Config.new
      config.numbers = [1,2,3,4,5]
    end
  end
end

# The default App::Config
config = App.config

# Change the default.
App.config = App::Config.new

Then everything can use App.config by default.

class App
  class Tally
    # Your classes have no state, no need for an object.
    class << self
      def call(to_tally = App.config.numbers)
        raise TypeError if to_tally.any?{|num| num.class != Integer }

        to_tally.each(&:puts)
      end
    end
  end

  class Thing
    attr_accessor :config

    def initialize
      @config = App.config
    end
  end
end

# Uses App.config.numbers
App::Tally.call
# Uses user applied Array.
App::Tally.call([5, 4, 3, 2, 1])

# thing.config is App.config
thing = App::Thing.new
# or maybe you want to use a different config
thing.config = App::Config.new
  1. Write a Config class that is a fairly simple Value object.
  2. Provide a class method to get the default Config object.
  3. Everything uses that class method to get the default config.
    • But can also be provided with a different config for flexibility.
    • For example, testing.
0
max On

The typical patterns for making a gem configurable is to provide a simple method which yields a configuration object to to the block:

module MyGem
  def self.configuration
    @configuration
  end

  def self.configure(**attributes, &block)
    @configuration ||= Configuration.new
    @configuration.assign_attributes(**attributes) 
    yield(@configuration)
  end
end

This object is just a simple value object with some mass assignment tossed in:

module MyGem
  class Configuration
    attr_accessor :numbers, :foo, :bar, :baz

    def initialize(**attributes)
      assign_attributes(**attributes)
    end
  
    def assign_attributes(**attributes)
      attributes.each { |key, value| send "#{key}=", value }
    end
  end
end
MyGem.configure(foo: 'bar') do |config|
  config.numbers = [1, 2, 3, 4]
end

MyGem.configuration.numbers # [1, 2, 3, 4]
MyGem.configuration.foo # "bar"

Since MyGem.configuration is a module method there isn't exactly a convenvience method for calling it from other classes / modules in the same namespace. If you need that create such a method that just delegates to MyGem.configuration.