The objective of this article is to highlight the subtle differences between using class attributes and class methods in Ruby. Both offer a valid way to manipulate the state of an instance of a class. Things can get increasingly complex, hard to test and hard to maintain as more instance variables are introduced. Beginner and intermediate Rubyists often miss this subtle but important point which can introduce bugs that may be hard to fix in their native habitat.
The reasons I prefer to use methods over instance variables:
- Increases predictability of method calls
- Increases predictability when testing
- Allow for memoization of expensive calls
- Set default values
- Gracefully catch failures
Ruby's attribute accessors are shortcuts that allow you to get and set values for instance variables within a class. Ruby provides a few ways to accomplish this
attr_reader
- create a method to get the value of an instance variableattr_writer
- create a method to set the value of an instance variableattr_accessor
- creates method to both set and get value of an instance variable
You can read more about these basics in the Objects and Attributes from Programming Ruby.
Example of attr_accessor
:
class Appointment
attr_accessor :office
end
@appt = Appointment.new
# No default value after initialization
@appt.office
=> nil
# Set the value
@appt.office = 'Downtown'
# Read the newly assigned value
@appt.office
=> 'Downtown'
Now the difference with attr_reader
. Note here that we will not be able to set the value for the instance variable.
class Appointment
attr_reader :office
end
@appt = Appointment.new
# No default value after initialization
@appt.office
=> nil
# Set the value
@appt.office = 'Somewhere'
=> NoMethodError: undefined method `office=` for #<Appointment:0x202342332>
Essentially the attr_reader
, attr_accessor
, and attr_writer
are magically creating methods for you. In the case of our attribute office
, the attr_reader
above is dynamically creating a method office=
which allows value assignment.
We can manually accomplish the same magic as attr_accessor
by explicitly declaring those methods
class Appointment
def office=(place)
@office = place
end
def office
@office
end
end
These are the basic building blocks of Ruby classes. Classes become more complex, adding more methods that may mutate the state of many instance variables within a class. Let's examine the usage of attribute accessors and class methods in a slightly more complicated class.
class Appointment
attr_accessor :last_appointment, :next_appointment, :office
def schedule(office)
@next_appointment = @last_appointment.to_i + 60*60*24*30
@office = office
send_reminder(@next_appointment, @office)
end
def notify
puts "Next appointment is #{@next_appointment}."
end
end
@appt = Appointment.new
@appt.last_appointment
=> nil
# Access the instance variable directly
@appt.next_appointment
=> nil
# Method that relies on the value of `@next_appointment`
@appt.upcoming_appointments
=> "Next appointment is ."
@appt.office
=> nil
# Method that relies on both `@next_appointment` and `@last_appointment`
@appt.schedule('Downtown')
=> nil
@appt.notify
=> "Next appointment is 2592000."
There are several undesirable effects here:
- Calling
#upcoming_appointments
gives us two different answers depending on the calling order. The result should be consistent between calls @last_appointment
was nil when called. The value was never set.@next_appointment
relies on@last_appointment
which also was never set. This also returns the wrong value.
Potential solutions to these problems:
- Initialize
@next_appointment
,@last_appointment
inAppointment#initialize
A typical solution. In many object oriented languages you pass all the data you need to the constructor. I personally don't like this because everything must then be routed through #initialize
. Example:
def initialize(place)
@last_appointment = get_last_appointment_from_database
@office = place
end
- Explicitly set the value for
@last_appointment
elsewhere
I see this pattern very often in Ruby code. I strongly dislike this because it forces you to clutter other parts of your code with an implementation that really belongs elsewhere. This is messy because your behavior has now leaked into other parts of your code, thereby making it harder to test and more importantly harder to refactor or fix. When you want to change anything, you now have to search and replace those invocations. An example of this approach:
class Foo
# pseudocode in another class somewhere else
def update_patients
patients.each do |patient|
@appt = Appointment.new
@appt.last_appointment = get_last_appointment_from_database
@appt.schedule('Downtown')
@appt.save
patient.send_reminder
end
end
end
Let's rewrite the class using instance methods. The first step here is to remove the line attr_accessor :last_appointment, :next_appointment
. We must now explicitly declare those methods:
class Appointment
def schedule(location)
@office = location
send_reminder(next_appointment, office)
end
def notify
puts "Next appointment is #{next_appointment}."
end
def last_appointment
@last_appointment ||= get_last_appointment_from_database
rescue => exception
# Recover however you want
Time.now.to_i
end
def next_appointment
@next_appointment = last_appointment + 60*60*24*30
end
# "||=" sets a default value if none has been set
def office
@office ||= 'Downtown'
end
end
What changed?
- Remove the
@
prefix for the instance variables. This means we're now calling the methods instead of the instance variables. This is a hugely important change - Add a method
last_appointment
which sets the internal instance variable@last_appointment
- Add a method
next_appointment
which sets the internal instance variable@next_appointment
- Add a method
office
which sets a default value if none have been set
Let's see how that changes the output
@appt = Appointment.new
@appt.last_appointment
=> 1425214800
# Access the instance variable directly
@appt.next_appointment
=> 1427806800
# Method that relies on the value of `@next_appointment`
@appt.upcoming_appointments
=> "Next appointment is 1427806800."
@appt.office
=> 'Downtown'
# Method that relies on both `@next_appointment` and `@last_appointment`
@appt.schedule('Suburbs')
=> 'Suburbs'
@appt.office
=> 'Suburbs'
Benefits of changes:
-
#office
now has a default value. -
last_appointment
is now memoized. Memoization is a technique that allows caching of class internals. This can be extremely useful if you're doing operations that are very expensive - calling a database, calling an external web service, etc.You can also rescue exceptions. "Expensive" is often synonymous with "unreliable". When you call external services you open the door to a variety of errors - timeouts, 500s, unexpected responses. Defining the method in this way allows us to wrap that call in a rescue block so that our code will fail elegantly.
-
Values for instance attributes are only updated when they should be. Not using an
attr_accessor
forlast_appointment
prevents other classes from modifying that value (although if we used anattr_reader
that would not be a problem...).
The output for these calls is now far more consistent and predictable than it was before. This is a big advantage not only for where the class is being used but also in regards to the tests. The key takeaway is that you should be using instance methods where possible within your class. Avoid using instance variables (i.e. @variable
) where possible except when you're setting the values.