Leveraging Instance Variables in Ruby Classes

Hint - Prefer Methods over Instance Variables

I previously published this in 2015 at a different site, but I think it remains helpful for those beginning their Ruby 📖 journey.

In I started mentoring for The Firehose Project; an online Rails bootcamp.

What follows is an introduction I previously wrote in that remains evergreen. This is an introduction to leveraging instance variables in Ruby.

Along the way, I’ll talk about my preferred class methodology as it relates to setting instance variable vs. calling setter methods.

# Example 1
class Book
  def initialize(a_title)
    @title = a_title
  end

  def title
    @title
  end
end

This can be condensed into the following:

# Example 2
class Book
  def initialize(a_title)
    @title = a_title
  end

  attr_reader :title
end

More and more I prefer to not set instance variables as part of initialization, but instead call setter methods (more on that later).

# Example 3
class Book
  def initialize(a_title)
    # Make sure to remember to use `self.title = a_title` instead of
    # `title = a_title`. If you use `title = a_title`, you will be setting the
    # local variable `title` instead of calling the instance method `#title=`
    # (and thus setting the instance variable `@title`)
    self.title = a_title
  end

  attr_reader :title

  def title=(a_title)
    @title = a_title
  end
end

This can be condensed into the following:

# Example 4
class Book
  def initialize(a_title)
    self.title = a_title
  end

  attr_reader :title
  attr_writer :title
end

And further into:

# Example 5
class Book
  def initialize(a_title)
    self.title = a_title
  end

  attr_accessor :title
end

Using attr_accessor and instance method setters on initialize is my preferred methodology (as in Example #5). Previously, I’ve used Example #2 as my defacto methodology. However, I’m less inclined to use that these days.

My reasoning for using a setter method is as follows:

  • I am encapsulating how the instance variable is ultimately set.
  • I am easing any work that may need to be done to extend the Book class.

Consider the following:

# Example 6
class FrenchBook < Book
  def title=(a_title)
    @title = translate_to_french(a_title)
  end
end

In using the setter method, the setter method is an inflection point—a place to extend and modify—in the code.

If I were setting the instance variable in the initialize method, my inflection point would be the initialize method. The following example illustrates that change.

# Example 7
class Book
  def initialize(a_title)
    @title = a_title
  end
  attr_reader :title
end

class FrenchBook < Book
  def initialize(a_title)
    @title = translate_to_french(a_title)
  end
end

The overall lines of code for both examples are the same. But consider what happens when we need to change the Book class.

# Example 8
class Book
  def initialize(a_title, an_author)
    # do stuff here
  end
end

The FrenchBook class of Example 7 would need to change.

Conclusion

Using attr_writer instead of setting an instance variable is a quick means of separating the concerns of your methods. Just as a class should have a single responsibility (i.e. a reason to change), so too should your methods.

By using a method to set your instance variable, you are making it easier for extension, modification, and overall maintenance.

What follows is my preferred class structure these days:

# Example 9
class Book
  def initialize(a_title)
    self.title = a_title
  end

  attr_accessor :title
  private :title=
end

Outside of object instantiation, I don’t want to expose any means of updating the title. I want to protect from outside forces the mutability of the object’s state.