attr_reader method can be used to automatically generate a “getter” for a given instance variable.
In simple terms, doing
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.
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 public def initialize(owner:, license_plate:) # ... end # ... end
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_acessors, 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
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
So instead let’s not access the instance variables directly, but let’s use the methods that the
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
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.