De-tangling attr_reader, attr_writer and attr_accessor from attr_protected & attr_accessible (Part 1)

Car crashWhat a mess. You have undoubtedly run across these methods sprinkled throughout the Ruby and Rails world. If you've been working with Rails for even a short time, you've probably read a little about security and attr_accessible. But do you really understand what each of them do and when to use them?
I'm not a fan of whoever made these methods so closely named, especially because they serve very different purposes: two are specific to Rails (or more accurately, ActiveRecord), while the other three are Ruby core methods. When I have a need for any of them, I still have to really think about which one I actually want to use. Often I still have to glance at the rdocs to be reassured my choice is the right one. So let's dive in and figure out what the heck these are supposed to do, and when to use them.

Forget Rails, Let's Talk Ruby Attributes

I've noticed a fair number of folks try to write Rails apps without much knowledge of Ruby. That's a noble thing to try, but to be a truly effective Rails developer, you'll need to be intimately knowledgeable about the Ruby language.

For this reason, and to get our feet wet, we'll ignore the Rails methods for a moment and focus on the Ruby, since that's the source of three important attr_* methods: attr_accessor, attr_reader, and attr_writer. Put these three methods together in a bucket inside your head, as they are closely related.

Say we have a simple little Ruby class (remember we're ignoring Rails at the moment, so this is just a basic Ruby Object class, not an ActiveRecord model).

In address.rb:

class Address

   def initialize(line1, line2, city, state, zip)
     @line1 = line1
     @line2 = line2
     @city = city
     @state = state
     @zip = zip
   end

   def to_s
     "#{@line1}\n#{@line2}\n#{@city}, #{@state} #{@zip}"
   end

end

Our class doesn't do much yet, but we can create an Address instance and print it as a string using the to_s method:

irb(main):001:0> require 'address'
=> true
irb(main):002:0> address = Address.new("Centropy", "PO Box 1236", "Santa Clara", "CA", "95052")
=> #<Address:0x85908 @line2="PO Box 1236" @zip="95052" @line1="Centropy" @state="CA" @city="Santa Clara">
irb(main):003:0> puts address.to_s
Centropy
PO Box 1236
Santa Clara, CA 95052

What if we want to be able to get each of those attributes separately from the full address, say to print out only the zip code? Trying to print the zip attribute with our current code won't work:

irb(main):004:0> puts address.zip
NoMethodError: undefined method `zip' for #<Address:0x85908>
        from (irb):4
        from :0

In order to call address.zip, we need an attribute getter to make the @zip instance variable visible outside the instance. If you're from the world of Java you're probably intimately familiar with these so-called "getter" methods. So, you'd go and write your "getter" method for @zip:

class Address

   def initialize(line1, line2, city, state, zip)
     @line1 = line1
     @line2 = line2
     @city = city
     @state = state
     @zip = zip
   end

   def zip
     @zip
   end

   def to_s
     "#{@line1}\n#{@line2}\n#{@city}, #{@state} #{@zip}"
   end

end

Okay, only four more methods to go and you can head over to the water cooler for a well-deserved break! (You get paid by the line, after all!) It feels kinda silly, though, since all these methods follow an identical pattern. Since this is such a common pattern, Ruby gives us a little shortcut: anytime you would add an attribute getter as above, you can and should use the handy attr_reader method instead, which effectively creates the attribute getter methods for us behind the scenes, without us having to explicitly define each method in detail. Our new class looks like this:

class Address

   def initialize(line1, line2, city, state, zip)
     @line1 = line1
     @line2 = line2
     @city = city
     @state = state
     @zip = zip
   end

   # Note we can give attr_reader multiple attributes,
   # and they are specified as :symbols
   attr_reader :line1, :line2, :city, :state, :zip

   def to_s
     "#{@line1}\n#{@line2}\n#{@city}, #{@state} #{@zip}"
   end

end

Now we can grab all of our attributes apart from the full address:

irb(main):001:0> require 'address'
=> true
irb(main):002:0> address = Address.new("Centropy", "PO Box 1236", "Santa Clara", "CA", "95052")
=> #<Address:0x854bc @line2="PO Box 1236" @zip="95052" @line1="Centropy" @state="CA" @city="Santa Clara">
irb(main):003:0> puts address.zip
95052

But what about setting the zip code?

irb(main):004:0> address.zip='95050'
NoMethodError: undefined method `zip=' for #<Address:0x854bc>
        from (irb):4
        from :0

Nope, doesn't work. Maybe that's what you want: if an attribute should not be settable after the object is instantiated, you wouldn't want the ability to set the value of an attribute like this. (If you ever hear the term "immutable", that is what we're talking about here: Address instances are currently immutable because they can not be "mutated," or changed, after instantiation.) But if you do want to set your attribute values, you'll need to create a way for code outside the instance to change the value of that somewhat elusive @zip instance variable. Again, if you are coming from Java programming, you'd attempt to write a "setter" method here:

class Address

   def initialize(line1, line2, city, state, zip)
     @line1 = line1
     @line2 = line2
     @city = city
     @state = state
     @zip = zip
   end

   # Note we can give attr_reader multiple attributes,
   # and they are specified as :symbols
   attr_reader :line1, :line2, :city, :state, :zip

   # Allow setting the instance attribute to a new value
   def zip=(new_zip)
     @zip = new_zip
   end

   def to_s
     "#{@line1}\n#{@line2}\n#{@city}, #{@state} #{@zip}"
   end

end

But, if you're starting to hear harp music right now, you might be thinking that Ruby might give us an equally simple, declarative way to make attribute setter methods as it did for getters. Lo and behold, attr_writer comes to the rescue:

class Address

   def initialize(line1, line2, city, state, zip)
     @line1 = line1
     @line2 = line2
     @city = city
     @state = state
     @zip = zip
   end

   # Note we can give attr_reader multiple attributes,
   # and they are specified as :symbols
   attr_reader :line1, :line2, :city, :state, :zip

   # Those attributes are writable, too
   attr_writer :line1, :line2, :city, :state, :zip

   def to_s
     "#{@line1}\n#{@line2}\n#{@city}, #{@state} #{@zip}"
   end

end

And now we can set or get any attribute value:

irb(main):001:0> require 'address'
=> true
irb(main):002:0> address = Address.new("Centropy", "PO Box 1236", "Santa Clara", "CA", "95052")
=> #<Address:0x8519c @line2="PO Box 1236" @zip="95052" @line1="Centropy" @state="CA" @city="Santa Clara">
irb(main):003:0> address.zip='95050'
=> "95050"
irb(main):004:0> puts address.zip
95050

Now, you might think we're in good shape: we've consolidated what would have been ten different method definitions into a two lines of declarative, readable code. Not so fast! There's a bit of redundancy in those two lines, isn't there? Specifying all your fields in two places is clearly less maintainable than if we could tell Ruby "we want getters and setters for this list of attributes." It might not be a big deal for our little Address class, but when you have hundreds or thousands of classes that you haven't looked at in a year, every extraneous line matters.

Again, this is such a common scenario in programming that Ruby offers the third attribute method, attr_accessor, which does exactly what two separate calls to attr_reader and attr_writer would do.

So, let's get rid of that repetition now:

class Address

   def initialize(line1, line2, city, state, zip)
     @line1 = line1
     @line2 = line2
     @city = city
     @state = state
     @zip = zip
   end

   # attr_accessor can take multiple attributes like the others,
   # and they are specified as :symbols
   attr_accessor :line1, :line2, :city, :state, :zip

   def to_s
     "#{@line1}\n#{@line2}\n#{@city}, #{@state} #{@zip}"
   end

end

And this works exactly like the previous example:

irb(main):001:0> require 'address'
=> true
irb(main):002:0> address = Address.new("Centropy", "PO Box 1236", "Santa Clara", "CA", "95052")
=> #<Address:0x8519c @line2="PO Box 1236" @zip="95052" @line1="Centropy" @state="CA" @city="Santa Clara">
irb(main):003:0> address.zip='95050'
=> "95050"
irb(main):004:0> puts address.zip
95050

Hopefully this clarifies what these three methods are for. Next time, we'll explore the similarly-named-but-entirely-different-purposed attr_protected and attr_accessible methods brought to us by Rails' ActiveRecord models.

Further Reading

The attr_accessor, attr_reader, and attr_writer methods are fairly well documented in the Ruby core rdocs.

Feedback and Article Ideas

Want to see a topic explored here? Send Me a Message.