Reputation: 907
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?
Upvotes: 19
Views: 22681
Reputation: 1430
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
Upvotes: 2
Reputation: 661
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).
Upvotes: 43
Reputation: 23990
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.
Upvotes: 3
Reputation: 8293
This appears to work well:
class Foo
protected
@@bar = {}
def self.add_bar(val)
@@bar[self] ||= []
@@bar[self] += val
end
def self.get_bar
(self == Foo ? [] : @@bar[Foo] || []) + (@@bar[self] || [])
end
end
class Baz < Foo
end
Foo.add_bar ["a", "b"]
Baz.add_bar ["1", "2"]
Foo.get_bar # => ["a", "b"]
Baz.get_bar # => ["a", "b", "1", "2"]
Upvotes: 0