Quarktum
Quarktum

Reputation: 709

How to access class variables from module?

I was wondering how can I access class variables from modules

module Entity
  def foo
    # puts @@rules
  end
end

class Person
  include Entity

  attr_accessor :id, :name

  @@rules = [[:id, :int, :not_null],
             [:name, :string, :not_null]]
end

class Car
  include Entity

  attr_accessor :id, :year

  @@rules = [[:id, :string, :not_null],
             [:year:, :int, :not_null]]
end

p = Person.new
c = Car.new

p.foo # [[:id, :int, :not_null], [:name, :string, :not_null]]
c.foo # [[:id, :string, :not_null], [:year, :int, :not_null]]

I took a look at cattr_accessor and mattr_accessor from ActiveSupport, but still can't find a way to solve this.

Upvotes: 8

Views: 8344

Answers (4)

Max
Max

Reputation: 22315

Class variables in Ruby are weird when it comes to inheritance. Unless you know exactly what you're messing with there, it's best to avoid them. You might think you aren't using inheritance in this case, but what include actually does is inserts Entity into the ancestors of Person. See:

Person.ancestors
# [Person, Entity, Object, Kernel, BasicObject]

The particular behavior is tricky to describe, but the short version is that basically @@rules is shared between Entity, Person, and Car! Look:

Entity.class_variable_set(:@@rules, 'foo')
puts Car.class_variable_get(:@@rules)
# foo
puts Person.class_variable_get(:@@rules)
# foo

You probably don't want that!

It's better to use a class instance variable here, which is actually separate for each class.

module Entity 
  # create the class instance variable methods when this is included
  def self.included klass
    klass.singleton_class.send(:attr_reader, :rules)
  end

  def foo
    puts self.class.rules
  end
end

class Person
  include Entity

  attr_accessor :id, :name

  @rules = [[:id, :int, :not_null],
            [:name, :string, :not_null]]
end

class Car
  include Entity

  attr_accessor :id, :year

  @rules = [[:id, :string, :not_null],
            [:year, :int, :not_null]]
end

Upvotes: 12

Myst
Myst

Reputation: 19221

This isn't really my answer, it's a variation on @Max's answer, just without exposing the @rules variable (see @Quarktum's comments).

The difference here is that I'm using the #module_exec method, which allows instance variable access (unlike #module_eval).

Also, I'm defining the .foo and #foo methods under the scope of the including class, so that the methods are the classes methods rather than the module's methods (test with Car.methods false to view Car's methods without inheritance).

module Entity 
  # create the class instance variable methods when this is included
  def self.included klass
    klass.module_exec do
      @rules ||= []
      def self.foo
        puts @rules
      end
      def foo
        self.class.foo
      end
    end
  end
end

class Person
  include Entity

  attr_accessor :id, :name

  @rules = [[:id, :int, :not_null],
                [:name, :string, :not_null]]
end

class Car
  include Entity

  attr_accessor :id, :year

  @rules = [[:id, :string, :not_null],
                [:year, :int, :not_null]]
end

Upvotes: 0

Jerry
Jerry

Reputation: 884

In addition to the already given answers, here's something I found out sometime back.

module MyModule
  @@my_variable = 5
  define_singleton_method(:my_variable) do
    @@my_variable
  end
end

Now you'll be able to access the class variable in two ways: MyModule::my_variable or MyModule.my_variable.

This now works like an attr_reader. You can define a second singleton method for assignment.

Upvotes: 2

Charlie
Charlie

Reputation: 7349

It's not the most elegant solution but class_eval works:

module Entity
  def foo
    self.class.class_eval('@@rules')
  end
end

Edit: Actually slightly cleaner may be to use class_variable_get

module Entity
  def foo
    self.class.class_variable_get(:@@rules)
  end
end

Upvotes: 2

Related Questions