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!
