Today we’ll talk about attributes in Ruby.
Let’s start with the following rule from the Ruby Style Guide:
attrfamily 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
attr_accessor do exactly that kind
of magic. Here’s an example using
# 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
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’s behavior changed between Ruby 1.8 and 1.9. In Ruby 1.8
created a single reader method. With an optional second boolean argument it
created both a reader and a writer method (a la
# 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
# 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
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
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
? at the end of predicate method names, but this cannot be done with
attr_reader/attr_accessor. Some people would simple hand-code such
def something? @something end
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).
# 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
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!