I am working on a small rails app and have a problem with ruby's OOP model. I have the following simplified class structure.
class Foo
protected
@bar = []
def self.add_bar(val)
@bar += val
end
def self.get_bar
@bar
end
end
class Baz < Foo
add_bar ["a", "b", "c"]
end
My problem is now, that when I call add_bar in the class definition of Baz, @bar
is apparently not initialized and I get an error that the +
Operator is not available for nil
. Calling add_bar
on Foo
directly does not yield this problem. Why is that and how can I initialize @bar
correctly?
To make clear what I want, I will point out the behavior I would expect from these classes.
Foo.add_bar ["a", "b"]
Baz.add_bar ["1", "2"]
Foo.get_bar # => ["a", "b"]
Baz.get_bar # => ["a", "b", "1", "2"]
How could I achieve this?
You can use the method Object#instance_variables to list all instance variables of an object. You normally “declare” and initialize all the instance variables in the initialize method.
Instance variables can be initialized in constructors, where error handling or other logic can be used. To provide the same capability for class variables, the Java programming language includes static initialization blocks.
Short answer: instance variables don't get inherited by subclasses
Longer answer: the problem is that you wrote @bar = []
in the body of the class (outside any method). When you set an instance variable, it is stored on whatever is currently self
. When you're in a class body, self
is the class object Foo. So, in your example, @foo
gets defined on the class object Foo.
Later, when you try to look up an instance variable, Ruby looks in whatever is currently self
. When you call add_bar from Baz, self
is Baz. Also self
is STILL Baz in the body of add_bar (even though that method is in Foo). So, Ruby looks for @bar
in Baz and can't find it (because you defined it in Foo).
Here's an example that might make this clearer
class Foo
@bar = "I'm defined on the class object Foo. self is #{self}"
def self.get_bar
puts "In the class method. self is #{self}"
@bar
end
def get_bar
puts "In the instance method. self is #{self} (can't see @bar!)"
@bar
end
end
>> Foo.get_bar
In the class method. self is Foo
=> "I'm defined on the class object Foo. self is Foo"
>> Foo.new.get_bar
In the instance method. self is #<Foo:0x1056eaea0> (can't see @bar!)
=> nil
This is admittedly a bit confusing, and a common stumbling point for people new to Ruby, so don't feel bad. This concept finally clicked for me when I read the 'Metaprogramming' chapter in Programming Ruby (aka "The Pickaxe").
How I'd solve your problem: Look at Rails' class_attribute
method. It allows for the sort of thing you're trying to do (defining an attribute on a parent class that can get inherited (and overidden) in its subclasses).
Well, since @bar is defined as class instance variable then it's limited to the class Foo. Check this:
class Foo
@bar = []
end
class Baz < Foo
end
Foo.instance_variables #=> [:@bar]
Baz.instance_variables #=> []
Anyway, for this simple example you can do this:
class Foo
protected
def self.add_bar(val)
@bar ||=[]
@bar += val
end
def self.get_bar
@bar
end
end
class Baz < Foo
add_bar ["a", "b", "c"]
end
Read more about this here.
I do it like so:
class Base
class << self
attr_accessor :some_var
def set_some_var(value)
self.some_var = value
end
end
end
class SubClass1 < Base
set_some_var :foo
end
class SubClass2 < Base
set_some_var :bar
end
Then it should do what you want.
[8] pry(main)> puts SubClass1.some_var
foo
[9] pry(main)> puts SubClass2.some_var
bar
Note that the set_some_var method is optional, you can do SubClass1.some_var = ...
if you prefer.
If you want some default value, add something like that under class << self
def some_var
@some_var || 'default value'
end
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