why i always use attr_reader to access instance variables
Ruby’s attr_reader
method can be used to automatically generate a “getter” for a given instance variable.
In simple terms, doing
attr_reader :engine
is the same as writing
def engine @engine end
I’ve adopted a programming style where I always try to use attr_reader
to access instance variables (or getters, if you prefer to call them that 😁). I’ve been asked about the reasons that led me to adopt this style in several PRs, so I wanted to write a quick post motivating my choice.
As a driving running example, I will use the following class to represent a car:
class Car attr_reader :owner attr_reader :license_plate private attr_reader :engine attr_reader :gearbox def initialize(owner:, license_plate:) # ... end public # ... end
💡 Note: An earlier version of this post put the public
before the initialize
. In practice, doing this doesn’t affect the initialize
(try it out for yourself!); but to make it even more clear, I’ve moved the public
below that definition – thanks Victor Goff for an insightful discussion on this matter.
And here’s my reasoning:
Placing all attr_readers at the top of the class
This serves as documentation: You can open up a class and immediately spot what its internal state is.
Furthermore, if your code is well-behaved and there’s no attr_writer
s or attr_acessor
s, you can quickly know if a class is mutable (its internal state can change during its lifetime) or immutable (its internal state cannot change after initialization — note that this only covers the object itself, and not the objects it references, e.g. if the variable points at an unfrozen array).
By following this simple code discipline, you can get a lot of useful information at a glance.
Using the attr_readers to access instance variables from inside the class
This helps avoid typing mistakes. For instance, consider we add the following method to our Car
class, but mistakenly add a typo while writing it (e.g. @eNgine
instead of @engine
):
def start @gearbox.neutral @eNgine.start end
What happens when we try to run our code?
$ ruby car.rb car.rb:20:in `start': undefined method `start' for nil:NilClass (NoMethodError) from car.rb:24:in `<main>'
“What? Why is my engine nil
?” you may ask yourself. And off you go on a wild goose chase trying to hunt down why your engine was not correctly initialized because you haven’t noticed that there’s a typo on your start
method.
So instead let’s not access the instance variables directly, but let’s use the methods that the attr_reader
created:
def start gearbox.neutral eNgine.start end
And what happens when we run this version?
$ ruby car.rb car.rb:20:in `start': undefined local variable or method `eNgine' for #<Car:0x000000c9d9522d50> (NameError) Did you mean? @engine from car.rb:24:in `<main>'
Our output is a lot clearer: there’s no eNgine
declared and “did you mean?” engine
. You immediately spot your typo and move on.
The usual advantages of using a getter
When you use a method to access your instance variable, it becomes a lot easier to later refactor it, without needing to update all the places where it is used.
For instance, consider that we wanted to lazily initialize an engine whenever one is not provided when our car object is created. We can easily change our
attr_reader :engine
into
def engine @engine ||= EngineFactory.create end
Done! This saves us from hunting down usages of @engine
in our class and its subclasses.
…Even for private instance variables…
Because all of the advantages above are applicable to instance variables that are not exposed to the outside of the class, I recommend using attr_reader
for these variables too, even if you would not usually do so.