3 minute read

Today we’ll talk about attributes in Ruby.

Let’s start with the following rule from the Ruby Style Guide:

Use the attr family of functions to define trivial accessors or mutators.

Everyone who’s coded a bit of Ruby knows it’s preferable to generate trivial reader and writer methods via some metaprogramming magic instead of writing them by hand. The methods from Module attr, attr_reader, attr_writer and attr_accessor do exactly that kind of magic. Here’s an example using attr_reader:

# bad
class Person
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def first_name
    @first_name
  end

  def last_name
    @last_name
  end
end

# good
class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

Here’s attr_writer in action:

# bad
class Person
  def initialize(name, name)
    @name = name
  end

  def name=(name)
    @name = name
  end
end

# good
class Person
  attr_writer :name

  def initialize(name)
    @name = name
  end
end

attr_accessor combines the two:

# bad
class Person
  def initialize(name, name)
    @name = name
  end

  def name
    @name
  end

  def name=(name)
    @name = name
  end
end

# good
class Person
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

Pretty sure none of you has learned anything new at this point. Now we start with the fun part…

How many of you know how attr behaves? Are you totally sure? Let’s see what the style guide says about it:

Avoid the use of attr. Use attr_reader and attr_accessor instead.

attr’s behavior changed between Ruby 1.8 and 1.9. In Ruby 1.8 attr created a single reader method. With an optional second boolean argument it created both a reader and a writer method (a la attr_accessor).

# Ruby 1.8
# same as attr_reader :something
attr :something

# creates a single attribute accessor (deprecated in 1.9) - same as attr_accessor :something
attr :something, true

# can't do this
attr :one, :two, :three

Note that you cannot pass multiple attribute names to attr in Ruby 1.8.

In Ruby 1.9 calling attr with an attribute name and a boolean is deprecated and it now behaves a lot more like attr_reader.

# Ruby 1.9
attr :one, :two, :three # behaves as attr_reader

Given all this facts it’s not a surprise that so many people think it’s a bad idea to use attr. I guess if the design of that portion of the API were up to me I’d have made attr behave like attr_accessor from day 1. The name of attr_accessor is a bit of a misnomer since accessor is hardly a synonym for reader and writer. Anyways, this is not of particular importance. Off to the next item on our agenda for today.

Is this something that should have been defined with attr_reader?

def something
  @something_else
end

Basically it call comes down to whether this is a trivial reader method or not. Some people would argue that because the name of the instance variable and the name of the method are not the same - it’s not. I’d argue the opposite case - it is! The name of the method does not change the semantics. In essence you’re simply in need of an alias for the default attribute reader method:

attr_reader :something_else
alias_method :something, :something_else

Boolean attributes are a bit special, since generally we’d like to have a ? at the end of predicate method names, but this cannot be done with attr_reader/attr_accessor. Some people would simple hand-code such methods:

def something?
  @something
end

I’d employ alias_method again:

attr_reader :something
alias_method :something?, :something

I wouldn’t call one style necessary good or bad - it’s more of a personal preference.

One final note - you should use attr_ only for trivial reader and writer methods (trivial means that they do not need any defensive copying or pre-update checks).

Consider this:

# attr_reader generates code like this
def something
  @something
end

This would expose @something to external modifications if it’s a mutable object. To shield yourself from this you can use defensive copying (or freezing when applicable):

# defensive copying in action
def something
  @something.dup # return a copy of @something
end

Same goes for attributes writers. If you have an age attribute and you want to enforce that it should be a positive number you’d generally roll your own writer:

def age=(age)
  fail 'Age should be a positive number!' unless age > 0

  @age = age
end

This post has become way too long, so I’ll be wrapping it up. I hope you’ve found my musing on the subject of attributes useful.

As usual I’m looking forward to hearing your thoughts here and on Twitter!

P.S. Happy 4th of July to all my American readers!