Understanding class instance variables in Ruby
It is well known that Ruby has instance and class variables, just like any Object-Oriented language. They are both widely used, and you can recognize them by the @a
and @@a
notation respectively.
Yet sometimes these tools are not enough to solve certain kinds of problems, most of them involving inheritance.
For example, let's model a domain where there are some owners who own dogs. These owners can be Nerds, Emos or Haters, and each type has certain preferred dog names that will be useful data at some point of the program. We could try and store this dog names as Nerd, Emo or Hater class variables (by the way, Hater does not prefer any name). We should also implement a DogMixin to encapsulate the assignment and retrieval of dog names.
A first version of the program would look like this:
module DogMixin
class << self
def included(base)
base.extend ClassMethods
end
end
module ClassMethods
# Assigns the preferred dog names to a class variable.
def assign(*names)
@@dog_names = names
end
def dog_names
@@dog_names
end
end
end
class Owner
include DogMixin
end
class Nerd < Owner
assign :r2d2, :posix
end
class Emo < Owner
assign :bill, :tom
end
class Hater < Owner
end
p Nerd.dog_names
# => [:bill, :tom]
p Emo.dog_names
# => [:bill, :tom]
p Hater.dog_names
# => [:bill, :tom]
Wait... Nerd prefers Bill and Tom as dog names? And Hater? We were told that Hater does not prefer any name! Let's take a deeper look into the assignment part:
module ClassMethods
# Assigns the preferred dog names to a class variable.
def assign(*dogs)
@@dog_names = dogs
end
# ...
end
The problem is that when we assign the names to @@dog_names
, we are binding them to DogMixin::ClassMethods::@@dog_names
, not the Nerd, Emo or Hater names. This means @@dog_names
always refers to the module class variable, which is the same for everyone, and is changed every time a class calls .assign *names
.
Ok, up until now there's only been a little metaprogramming confusion! Let's give it a try using class_variable_set
and class_variable_get
:
module DogMixin
class << self
def included(base)
base.extend ClassMethods
end
end
module ClassMethods
def assign(*names)
class_variable_set(:@@dog_names, names)
end
def dog_names
class_variable_get(:@@dog_names)
end
end
end
class Owner
include DogMixin
end
class Nerd < Owner
assign :r2d2, :posix
end
class Emo < Owner
assign :bill, :tom
end
class Hater < Owner
end
p Nerd.dog_names
# => [:r2d2, :posix]
p Emo.dog_names
# => [:bill, :tom]
p Hater.dog_names
# => uninitialized class variable @@dog_names in Hater (NameError)
Now every Nerd has proper preferred dog names! And Emos can keep adoring the Kaulitz brothers. But what's with the Hater? Since it never called .assign
, retrieving the dog names raises an error.
Did you just say class instance variables?
Here we shall introduce a bit of a weird concept: class instance variables. Basically you can recognize them because they look like instance variables, but you'll find them on a class level.
They work like a regular class variable, but they differ with those because they are not shared with subclasses. They belong exclusively to the class itself.
You can think of them as "instance variables of the object X", where X is a particular class (remember that classes are also objects in Ruby!). Inception.
Before diving further into the dog owner domain, let's take a little break and see a practical example with a typical instance counter. In the first step we are using regular class variables:
class Person
# @@count is a class variable shared by Person and every subclass.
# When you instantiate a Person or any kind of Person, such as a Worker,
# the count increases.
@@count = 0
def initialize
self.class.count += 1
end
def self.count
@@count
end
def self.count=(value)
@@count = value
end
end
class Worker < Person
end
8.times { Person.new }
4.times { Worker.new }
p Person.count # => 12
p Worker.count # => 12
The counter is obviously shared by Person and all of its subclasses. Let's take the class instance variables approach and make the counter exclusive to each class:
class Person
# @count is a CLASS INSTANCE VARIABLE exclusive to Person.
# Only when you instantiate a Person (not a subclass of Person),
# the count increases.
@count = 0
def initialize
self.class.count += 1
end
def self.count
@count
end
def self.count=(value)
@count = value
end
end
class Worker < Person
# @count is a CLASS INSTANCE VARIABLE exclusive to Worker.
# Only when you instantiate a Worker, the count increases.
@count = 0
end
8.times { Person.new }
4.times { Worker.new }
p Person.count # => 8
p Worker.count # => 4
Let's apply this to the real life!
Meaning the dog problem.
module DogMixin
class << self
def included(base)
base.extend ClassMethods
end
end
module ClassMethods
def assign(*names)
# @dogs is bound to the EACH DogOwner subclass
@dog_names = names
end
def dog_names
# @dogs is bound to the EACH DogOwner subclass
@dog_names
end
end
end
class Owner
include DogMixin
end
class Nerd < Owner
assign :r2d2, :posix
end
class Emo < Owner
assign :bill, :tom
end
class Hater < Owner
end
p Nerd.dog_names
# => [:r2d2, :posix]
p Emo.dog_names
# => [:bill, :tom]
p Hater.dog_names
# => nil
Yay! You may now use this in production environments :)
If you know other use cases where class instance variables are useful, please leave them in the comments!
UPDATE: Xavier Noria (@fxn) gave us a clearer definition of these variables: They are just like regular instance variables. Instance variables are resolved to the current self, which is the class at top-level. Thanks Xavier!