Ruby Instance Variables as Class Variables
In one of my introductory lessons on object-oriented programming, I was given three classes to use which were identical apart from one or two methods or variables (this made sense since they all represented sub-types of one overall type of data).
Just for fun, I tried to "DRY" things up using class inheritance. I wanted to make every line of shared code - variables, accessors, and all - inheritable from a parent class. (Full disclosure: I doubt the following represents any kind of best, or even good, practice - I simply wanted to see if I could do it).
Most of the shared code ran without a hitch when dumped into a parent class and inherited back down. Even for shared methods which had slight differences per class, reopening the methods and incorporating the common code using super
was no problem.
The only problem I actually ran into was the class variable @@all
and its associated methods, which each class used to maintain and query an array of its instances. Here's an example of the original "WET" code and its intended functionality:
class Kia
attr_accessor :name
@@all = []
def self.all
@@all
end
def initialize(name)
@name = name
self.class.all << self
end
end
class Hyundai
# same as class Kia
end
Kia.new("Rio")
Kia.new("Soul")
Hyundai.new("Sonata")
Hyundai.new("Elantra")
Kia.all
#=> [#<Kia:0x000000024f39e8 @name="Rio">,
#<Kia:0x000000031b8250 @name="Soul">]
Hyundai.all
#=> [#<Hyundai:0x0000000311f848 @name="Sonata">,
#<Hyundai:0x00000003068f80 @name="Elantra">]
Now, here was my original revised class hierarchy:
class Car
attr_accessor :name
@@all = []
def self.all
@@all
end
def initialize(name)
@name = name
self.class.all << self
end
end
class Kia < Car
end
class Hyundai < Car
end
And its unintuitive functionality:
Kia.new("Rio")
Kia.new("Soul")
Hyundai.new("Sonata")
Hyundai.new("Elantra")
Kia.all
#=> [#<Kia:0x00000002d81a60 @name="Rio">,
#<Kia:0x00000002cdcc90 @name="Soul">,
#<Hyundai:0x00000002c5e4a8 @name="Sonata">,
#<Hyundai:0x00000002bc6158 @name="Elantra">]
Hyundai.all
#=> [#<Kia:0x00000002d81a60 @name="Rio">,
#<Kia:0x00000002cdcc90 @name="Soul">,
#<Hyundai:0x00000002c5e4a8 @name="Sonata">,
#<Hyundai:0x00000002bc6158 @name="Elantra">]
What's going on here? You would think (I did, at least) that each class would get its own @@all
variable. Actually, even the parent Car
class returns the same:
Car.all
#=> [#<Kia:0x00000002d81a60 @name="Rio">,
#<Kia:0x00000002cdcc90 @name="Soul">,
#<Hyundai:0x00000002c5e4a8 @name="Sonata">,
#<Hyundai:0x00000002bc6158 @name="Elantra">]
Why are all three using the same variable? Because the scope of a class variable actually descends throughout the class hierarchy - that is, class variables are not only visible to their class and its instances, but also to classes and instances further down the hierarchy.
Even if we create a new class which inherits from Kia
, it will reference the same @@all
variable from its 'grandparent' Car
:
class KiaSubClass < Kia
end
KiaSubClass.all
#=> [#<Kia:0x00000002d81a60 @name="Rio">,
#<Kia:0x00000002cdcc90 @name="Soul">,
#<Hyundai:0x00000002c5e4a8 @name="Sonata">,
#<Hyundai:0x00000002bc6158 @name="Elantra">]
What to do? I found the solution I was looking for in The Well Grounded Rubyist: use an instance variable instead.
The code would look like this:
class Car
attr_accessor :name
def self.all
@all ||= []
end
def initialize(name)
@name = name
self.class.all << self
end
end
class Kia < Car
end
class Hyundai < Car
end
And the resulting functionality resembles the original WET implementation:
Kia.new("Rio")
Kia.new("Soul")
Hyundai.new("Sonata")
Hyundai.new("Elantra")
Kia.all
#=> [#<Kia:0x00000001a8d260 @name="Rio">,
#<Kia:0x000000019ee200 @name="Soul">]
Hyundai.all
#=> [#<Hyundai:0x0000000194c4f0 @name="Sonata">,
#<Hyundai:0x000000018a7ce8 @name="Elantra">]
Why does this work? Because the scope of an instance variable is limited to an individual object. The subclasses Kia
and Hyundai
are different objects from each other and from their parent class Car
, and so all three can have their own separate instance variable by the name of @all
without them bumping into one another.
class Car
...
def self.all
@all ||= []
end
def initialize(name)
...
self.class.all << self
end
end
How does this work? You'll notice that @all
is not declared directly within the body of Car
, but within the class method self.all
.1 When this method is called on Car
, Kia
, or Hyundai
(either explicitly or by the initialize method), an @all
variable will be created for that class object if it doesn't already exist.
Despite the term "instance variable", instances of the three classes will not receive an @all
variable, as it will only ever be declared within the context of the class objects themselves (you can't call a class method on an instance, after all).
While I was able to satisfy my curiosity via this trick, there are some quirks that could be considered user interface design failures:
- After creating multiple
Kia
andHyundai
cars,Car.all
still returns an empty array, which is correct behavior but a little counter-intuitive. If I had my way, for example, callingCar.all
would return an array of all cars regardless of manufacturer, whileKia.all
would return only Kia's, etc. - As it stands, each class only possesses an
@all
variable after it receives.all
method. If a separate class tried to query the@all
arrays of different car classes without going through the.all
methods, errors could be raised ornil
values could be returned instead of empty arrays. It would probably be ideal ifCar
and its subclasses could begin their existence with@all
assigned to an empty array.
-
The
||=
operator is basically shorthand for, "If the variable on the left exists, return it; otherwise, assign it to the value on the right and return it." Google "Ruby Double Pipe Equals" to find more information. ↩