Andrei Maxim

April 17, 2024

Instance Variable Access in Ruby

Instance variables (or instance attributes) in Ruby are prefixed with an @ sign:

class Person
  def initialize(salutation: nil, first_name:, last_name:)
    @salutation = salutation
    @first_name = first_name
    @last_name  = last_name
  end

  def name
    [ @salutation, @first_name, @last_name ].compact.join(" ")
  end
end

john = Person.new first_name: "John", last_name: "Doe"
john.name # => "John Doe"

sam = Person.new salutation: "Mr.", first_name: "Sam", last_name: "Johnson"
sam.name # => "Mr. Sam Johnson"

In the above example variables are accessed using the direct variable access pattern, as described by Kent Beck in Smalltalk Best Practice Patterns and it has been my preferred style, mainly because they easily stand out when you read the code and allows you to differentiate between object methods and attributes.

The other approach is to use the indirect variable access pattern, which can be done by defining accessors:

class Person
  attr_reader :salutation, :first_name, :last_name

  def initialize(salutation: nil, first_name:, last_name:)
    @salutation = salutation
    @first_name = first_name
    @last_name  = last_name
  end

  def name
    [ salutation, first_name, last_name ].compact.join(" ")
  end
end

john = Person.new first_name: "John", last_name: "Doe"
john.name # => "John Doe"

sam = Person.new salutation: "Mr.", first_name: "Sam", last_name: "Johnson"
sam.name # => "Mr. Sam Johnson"

The side effect of defining accessors is that they are now accessible by other objects, which might not be something you want:

john.name #=> "John"
sam.salutation #=> "Mr."

The solution is to define the accessors in a private section:

 class Person
  def initialize(salutation: nil, first_name:, last_name:)
    @salutation = salutation
    @first_name = first_name
    @last_name  = last_name
  end

  def name
    [ salutation, first_name, last_name ].compact.join(" ")
  end

  private

  attr_reader :salutation, :first_name, :last_name
end

john = Person.new first_name: "John", last_name: "Doe"
john.name # => "John Doe"
john.first_name # => NoMethodError: private method `first_name' called for an instance of Person

While it may seem that the main difference between indirect variable access and direct variable access patterns is mainly stylistic, it does have an impact when using inheritance because you can override methods, but you can't override instance variables.

For example, the salutation of the members of a royal house need to start with "HRH" from "His Royal Highness" or "Her Royal Highness" and has to include the title (the naming conventions for royalty and nobility are actually quite complex). If we used direct variable access, we would need to reimplement the name method:
 
class Person
  def initialize(salutation: nil, first_name:, last_name: nil)
    @salutation = salutation
    @first_name = first_name
    @last_name  = last_name
  end

  def name
    [ @salutation, @first_name, @last_name ].compact.join(" ")
  end
end

class Royalty < Person
  def initialize(title:, first_name:, last_name: nil)
    @title = title
    @first_name = first_name
    @last_name = last_name
  end
  
  def salutation
    "HRH #{@title}"
  end

  def name
    [ salutation, @first_name, @last_name ].compact.join(" ")
  end
end

charles = Royalty.new title: "King", first_name: "Charles"
charles.name # => "HRH King Charles"

This also means that if we plan to support name suffixes later on we'll need to change the name method in both the Person and Royalty classes.

On the other hand, if we use indirect variable access, we can override the salutation attribute to always start with "HRH", while adding a suffix will require a change to the name method only on the parent Person class:

class Person
  def initialize(salutation: nil, first_name:, last_name: nil, suffix: nil)
    @salutation = salutation
    @first_name = first_name
    @last_name  = last_name
    @suffix     = suffix
  end

  def name
    [ salutation, first_name, last_name, suffix ].compact.join(" ")
  end

  private

  attr_reader :salutation, :first_name, :last_name, :suffix
end

class Royalty < Person
  def initialize(title:, first_name:, last_name: nil, suffix: nil)
    @title = title
    @first_name = first_name
    @last_name = last_name
    @suffix = suffix
  end
  
  def salutation
    "HRH #{title}"
  end

  private

  attr_reader :title
end

charles = Royalty.new title: "King", first_name: "Charles", suffix: "III"
charles.name # => "HRH King Charles III"

However, as you can see in this rather contrived example, it is not often that we need to override an attribute in the parent class with a method in the child class, so, at the end of the day, it's mostly a stylistic decision. 

Also, as with every stylistic decision, it's much more important to be consistent, at least at the class level.

About Andrei Maxim

Professional Ruby guy.