The Elements of Style in Ruby #6: Attributes Redux
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
. Useattr_reader
andattr_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!