Consider that I have a module XYZ. When included in a class, it extends the base class and adds a class variable @@foo inside it. It also extends the class with the methods to do some get and set.
module XYZ
def self.included(base)
base.send :extend, ClassMethods
base.class_eval do
@@foo ||= []
end
end
module ClassMethods
def foo
class_variable_get("@@foo")
end
def foo=(arg)
class_variable_set("@@foo", arg)
end
def dynamic_set(*args)
foo += args # This doesn't work
end
def dynamic_set_2(*args)
class_variable_set("@@foo", class_variable_get("@@foo") + args)
end
end
end
Now, consider the usage:
class A
include XYZ
end
A.foo #=> []
A.class_variable_get("@@foo") #=> []
A.dynamic_set 1, 2 #=> NoMethodError: undefined method `+' for nil:NilClass
A.dynamic_set_2 1, 2 #=> [1,2]
A.foo #=> [1,2]
A.class_variable_get("@@foo") #=> [1,2]
The snippet makes sense and gets the work done, but I'm not able to figure out why A.dynamic_set 1, 2 didn't work.
Coming to the main part of the question - If I define a new class B as:
class B
include XYZ
end
B.foo #=> [1,2] => Why? How did B.foo get these values?
B.class_variable_get("@@foo") #=> [1,2] => Why?
B.dynamic_set_2 3, 4
B.foo #=> [1,2,3,4]
A.foo #=> [1,2,3,4]
Why are B and A sharing the same class variable when @@foo is defined on the class level (with class_eval)?
I understand about implications of using class variable and class instance variables. Just trying to figure out why this doesn't work as intended, to clear some concepts :)
I'm not able to figure out why A.dynamic_set 1, 2 didn't work.
Use an explicit receiver when calling setters:
def dynamic_set(*args)
foo += args # This doesn't work
self.foo += args # This DOES work
end
Coming to the main part of the question
TL;DR: Don’t use class variables. Use instance variables at class level.
module XYZ
def self.included(base)
base.send :extend, ClassMethods
base.class_eval do
@foo ||= []
end
end
module ClassMethods
def foo
instance_variable_get("@foo")
end
def foo=(arg)
instance_variable_set("@foo", arg)
end
def dynamic_set(*args)
self.foo += args # This doesn't work
end
def dynamic_set_2(*args)
class_variable_set("@foo", instance_variable_get("@foo") + args)
end
end
end
It’s worth to mention it in the answer.
module XYZ
def self.included(base)
base.class_eval { @@foo ||= [] }
end
end
The code above pollutes the XYZ class variables with @@foo because @@foo is hoisted in XYZ module.
base.class_variable_set(:@@foo, []) instead would not pollute XYZ.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With