How To Avoid The Ruby Class Variable Problem

May 8, 2018 - 3 min read
How To Avoid The Ruby Class Variable Problem
Share

Have you noticed the weirdeness yet? I know for a very long time, I didn’t see it.

You probably think class variables work like instance variables.

But they do not.

What is a class variable?

You’ve probably seen them here and there. They look a little different, just because they are preceded by the (@@) sign. Here’s a quick example of a class variable in action.

# foo.rb
class Foo
  @@class_var = 'Hello!'

  def self.read_it
    puts @@class_var
  end
end

Foo.read_it

And if you were to execute this little script, it would do exactly what you would expect. Nothing wrong here.

$ ruby ./foo.rb
Hello!

The principle of least surprise

There’s a silent secret that we’ve all got used to, and to be completely honest; it kind of spoils us. That secret is called the principle of least surprise and it refers to the fact that Ruby most often does what you expect.

You can rely on your intuition most of the time and things just work.

The broken promise

There’s one issue thought that’s been bugging me for a while, and I think it breaks that principle. It’s about the way ruby looks up class variables before assignment.

To illustrate this, let’s continue with the little example from above and add two subclasses (I’m just appending the following code to the same file foo.rb).

# ... continuing foo.rb with the following code

class Bar < Foo
  @@class_var = 'World'
end

class Baz < Foo
  @@class_var = 'Underworld'
end

Foo.read_it
Bar.read_it
Baz.read_it

Can you guess what the output of executing the script will be? If you do, you can probably skip the rest of the article.

$ ruby ./foo.rb
Hello!
Underworld
Underworld
Underworld

WHAT!?

If you’re stuned by that output, don’t worry. I had the same exact reaction when I first saw it; it’s why I’m writing this article in the first place.

So what just happened here?

The problem lies in the way Ruby resolves class variables.

If the variable is not defined in the current class, Ruby will go up the inheritance tree looking for it. If it finds it in a superclass, it sets it right then and there. Otherwise, it creates one in the current class and sets that.

The solution

Obviously, that behavior is not obvious 🙂 So how do you fix it?

Using class instance variables

Just like this title says, the solution is to use class instance variables instead.

Here’s how the code looks if you use class instance variables.

# foo.rb
class Foo
  @ivar = 'Hello!'

  def self.read_it
    puts @ivar
  end
end

Foo.read_it

class Bar < Foo
  @ivar = 'World'
end

class Baz < Foo
  @ivar = 'Underworld'
end

Foo.read_it
Bar.read_it
Baz.read_it

And if you run that, here’s what you will get.

$ ruby ./foo.rb
Hello!
Hello!
World
Underworld

That’s more like it. Or at least, that’s whay I would expect to see.

12 Project Ideas
Cezar Halmagean
Software development consultant with over a decade of experience in helping growing companies scale large Ruby on Rails applications. Has written about the process of building Ruby on Rails applications in RubyWeekly, SemaphoreCI, and Foundr.