Class Variables – A Ruby Gotcha
Class variables in Ruby have a bad name. It’s true, they are considered harmful, borderline evil. So why the bad rap? Well, most groans are about how they behave when inheritance enters the fray. Correction: It’s all about how they behave compared to what we expect.
To paint a picture of our little problem child lets look at what we expect with PHP.
<?php
class DemoClass {
public $myvar;
public function __construct() {
$this->myvar = "hello world : ";
}
public function getMyVar() {
echo $this->myvar;
}
}
class Demo2Class extends DemoClass {
public function __construct() {
$this->myvar = "goodbye world : ";
}
}
$demo1 = new DemoClass();
$demo2 = new Demo2Class();
$demo1->getMyVar();
$demo2->getMyVar();
$demo1->getMyVar();
// Produces
// hello world : goodbye world : hello world : "
That makes sense to me. We define a class with a concrete constructor and abstract getter. When inherited we can override the constructor and set the “class variable” to something else. The base class is unaffected, the world keeps spinning.
In Ruby if we try and do something similar using class variables we end up with one or two problems.
class DemoClass
@@my_var = nil
def initialize
@@my_var = "hello world"
end
def my_var
puts @@my_var
end
end
class Demo2Class < DemoClass
def initialize
@@my_var = "goodby world"
end
end
demo1 = DemoClass.new
demo1.my_var
demo2 = Demo2Class.new
demo2.my_var
demo1.my_var
# Produces
#hello world
#goodbye world
#goodbye world
When you approach class variables in Ruby in this manner they just seem wrong. The change to my_var
in Demo2Class
has bubbled up to our base class. To mitigate the problem, say we subclass DemoClass
again the behaviour of the class variable my_var
is nothing short of unpredictable. They are global variables in disguise.
So Why Use Class Variables?
On the face of it the rumours about class variables are true. But now we know what not to do, is there a scenario where class level variables are actually useful? Well, I confess. I use them in almost every web application.
Using the methods described in our examples, depending on initialization data implemented using class level variables as a class ‘state’ is a pretty bad idea. However, I do use class variables for initialization data.
Rails applications generally have at least 4 environments, production, staging, development and test. It’s very likely that configuration changes per environment, be it email addresses to mail exceptions to or a URL of a web service sandbox. The code itself looks like:
class AppConfig
@@app_config = YAML::load(ERB.new((IO.read("#{RAILS_ROOT}/config/app_config.yml"))).result)[RAILS_ENV]
def self.method_missing(sym, *args, &block)
@@app_config[sym.to_s]
end
end
It should be pretty self explanatory. A YAML file is parsed into the class variableapp_config. Then using
method_missingat class level I can retrieve values easily with
AppConfig.exception_email_addresses`.
Class Level Configuration
So now hopefully we can agree that class variables are not completely evil. Sometimes we do want to share traits at a class level down class hierarchy. A common technique to achieve this without incurring the problems described previously is to use class instance variables.
Although a bit of a mouthful, class instance variables are pretty straightforward, (coming from PHP they did not make sense at first) we just have to adjust our thinking slightly. I always find the best way to discover first is irb
.
ruby-1.9.2-p290 :001 > class Demo
ruby-1.9.2-p290 :002?> @my_instance_variable = "whaaa?"
ruby-1.9.2-p290 :003?> end
=> "whaaa?"
ruby-1.9.2-p290 :004 > Demo.class
=> Class
ruby-1.9.2-p290 :005 > Demo.superclass
=> Object
ruby-1.9.2-p290 :006 > Demo.instance_variables
=> [:my_instance_variable]
There it is, unlike more classical languages Ruby classes (like everything else) are just objects with instance variables. So how does this knowledge help us? Returning to the previous example and employing a little accessors trick.
class DemoClass
class << self
attr_accessor :my_var
end
@my_var = nil
def initialize
self.class.my_var = "hello world"
end
def my_var
puts self.class.my_var
end
end
class Demo2Class < DemoClass
def initialize
self.class.my_var = "goodby world"
end
end
demo1 = DemoClass.new
demo1.my_var
demo2 = Demo2Class.new
demo2.my_var
demo1.my_var
# Produces
# hello world
# goodbye world
# hello world
The example is looking pretty horrible now, before any explanation lets clean it up so it offends our eyes no longer.
class DemoClass
class << self
attr_accessor :my_var
end
@my_var = "hello world"
end
class Demo2Class < DemoClass
@my_var = "goodby world"
end
puts DemoClass.my_var
puts Demo2Class.my_var
puts DemoClass.my_var
If you are unsure about eigenclasses ( a.k.a singleton classes, but I’m originally a PHP guy and a singleton class meant something totally different at first) in this demonstration we are simply using it to create a attr_accessor
as we normally would, only doing it this way creates it for our class instance variable.
Wrapping up
Remember when I said class variables are not always bad and showed an example when they are perfectly acceptable (well in my book anyway). I did come across a problem with that AppConfig
snippet.
I had to duplicate an application for two different clients. Obviously the configuration data was different, but the codebase was to be the same. I wanted to maintain the codebase in a single SCM repository (bug fixes or updates would apply to both versions) and I didn’t want to hit git merge mashes, in fact I didn’t want the SCM to take on a new role in my workflow, it’s there to do SCM only, not manage configuration across clients.
Obviously, we would look to persist the configuration in a database, which is simple to achieve in Rails using an ActiveRecord
model like so:
# app/models/application_config.rb
class ApplicationConfig < ActiveRecord::Base
serialize :config_value
after_save :reset_config
def reset_config
AppConfig.reset_configs
end
end
# lib/app_config.rb
class AppConfig
class << self
attr_accessor :configs
end
@configs = {}
def self.method_missing(sym, *args, &block)
@configs[sym.to_s] ||= ""
end
def self.reset_configs
self.configs = {}
configs = ApplicationConfig.all
configs.each do |setting|
self.configs = {setting.config_key => setting.config_value}.merge self.configs
end
end
end
Using the code above I could drop in a persisted configuration for the application without refactoring any code where the configuration data was being accessed (this was a big win as it was a pretty large app). I could also use a simple scaffold to update and edit configuration on the fly. The use of class instance variables allowed me to cache the config data without hitting the database for every method_missing
message the class received.
Hopefully, that has dispelled the class variables are useless/evil myth. Like most things, a lack of understanding and naive implementations gives class variales a bad name. It certainly took me a while (and plenty of naive implementations) to get a grasp of what’s going on with them. As for the eigenclass stuff, check out Nathan Kleyns article on metaprogramming for more detail.