Boys Have Cooties and What Metaprogramming Taught Me About Attr_Accessor

March 11, 2013

What is metaprogramming?

After reading the The Rails Tutorial by Michael Hartl, I realized my Ruby on Rails journey wasn’t over. One of the interesting concepts I came across was metaprogramming. This post was heavily inspired by Paolo Perrotta’s wonderful book, Metaprogramming Ruby. In the case of Ruby, metaprogramming enables you to write code that writes other code dynamically. If this is hard to wrap your head around now, hopefully the upcoming example makes it clear.

What is attr_accessor?

You may have come across Ruby code like the following:

class Person
  def intialize
    @age = 12
  end

  def age=(n)
    @age = n
  end

  def age
    @age
  end
end

p = Person.new
p.age = 15 #=>15

attr_accessor gives you the “setter” and “getter” methods without having to define them

So the above Ruby code could be rewritten as:

class Person
  attr_accessor :age

  def intialize
    @age = 12
  end
end

p = Person.new
p.age = 15 #=>15

Ah, but how does Ruby do that?

It’s through metaprogramming. Let’s walk through a code example of implementing our own attr_accessor type method. The goal is to implement an attr_accessor type method that doesn’t let you set an age of a person that isn’t an even number.

Example: Implementing your own attr_accessor type method

The code:

module AttrCustom
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def attr_custom(attribute, &validation)

    define_method "#{attribute}=" do |value|
     raise "Your attribute does not match the block condition" unless validation.call(value)
     instance_variable_set("@#{attribute}",value)
    end

    define_method "#{attribute}" do
      instance_variable_get("@#{attribute}")
    end
  end

  end
end #AttrCustom

class Person
  include AttrCustom

  attr_custom :age do |val|
    val%2==0
  end

  def initialize
    @age = 2
  end
end

person1 = Person.new

person1.age = 15 #=> … block in attr_custom": Your attribute does not match the block condition (RuntimeError)

person1.age = 20

puts person1.age #=> 20

Include vs. extend

To understand the example code above, you have to understand a bit about the keywords include and extend. In Ruby, when you include a module in a class, you add instance methods to that class. When you extend a module in a class, you add class methods. This is best understood through a sample piece of code.

module Boy
  def cooties
    puts 'boys have cooties'
  end
end

class Girl
  include Boy
end

new_girl = Girl.new.cooties #=> boys have cooties

Girl.cooties # NoMethodError: undefined method ‘cooties’ for Girl:Class

class Cousin
  extend Boy
end

Cousin.cooties #=> boys have cooties

Cousin.new.cooties #=> NoMethodError: undefined method ‘cooties’ for # (NoMethodError)

You’ll see that by including the Boy module, a new instance of the Girl class will have the instance method cooties. But it will not have a class method for cooties. In contrast, by extending the Boy module, Cousin gets the class method cooties but not an instance method of it (as evidenced by the error above).

Back to attr_custom

Look at the following code again from the AttrCustom module:

def self.included(base)
  base.extend(ClassMethods)
end

The "self.included(base)" is telling Ruby that when an AttrCustom module is included in a class, then that class (i.e., base) should get extended with class methods from the ClassMethods module. In our specific case, it allows us to add the attr_custom method into our Person class when we include the AttrCustom module.

In an upcoming post, I’ll explain about the define_method call that enables you to define any custom attribute for our Person class.


Profile picture

Written by Bruce Park who lives and works in the USA building useful things. He is sometimes around on Twitter.