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

  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_writers or 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 @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.