Archives

Ruby 101: Improving Your Code by Defining Methods Dynamically

Let’s say you have a user and you want to check its role.

class User
  attr_accessor :role
end

u = User.new
u.role = 'admin'

# somewhere in your code you check the role

if u.role == 'admin'
  puts 'admin'
elsif u.role == 'moderator'
  puts 'moderator'
elsif u.role == 'guest'
  puts 'guest'
end

Using a string value is bad code and you can improve this by using constants instead. But still, this is bad code becauses it exposes implementation details of your User class.

For our first improvement, we define methods that check the user’s role and hide the implementation of the role checking inside the User class.

class User
  attr_accessor :role

  def is_admin?
    self.role == 'admin'
  end

  def is_moderator?
    self.role == 'moderator'
  end

  def is_guest?
    self.role == 'guest'
  end

end

u = User.new
u.role = 'guest'

if u.is_admin?
  puts 'admin'
elsif u.is_moderator?
  puts 'moderator'
elsif u.is_guest?
  puts 'guest'
end

Our first improvement is definitely better than the original but there are duplicate code in the role checking. You can eliminate the duplicate code by delegating the role checking to a single method.

class User
  attr_accessor :role

  def is_admin?
    is_role? 'admin'
  end

  def is_moderator?
    is_role? 'moderator'
  end

  def is_guest?
    is_role? 'guest'
  end

  protected

  def is_role?(name)
    self.role == name
  end

end

Our second improvement is a classic refactoring technique and common in any modern programming language. In other words, there is nothing “Ruby” about it. Before you get bored, I will now show the Ruby version.

The Ruby version uses #define_method to further eliminate duplicate code.

class User
  attr_accessor :role

  def self.has_role(name)
    define_method("is_#{name}?") do
      self.role == "#{name}"
    end
  end

  has_role :admin
  has_role :moderator
  has_role :guest

end

By using #define_method, we were able to add instance methods to our class User. You can check the new instance methods via irb.

ruby-1.9.2-p0 > User.instance_methods.grep /^is/
=> [:is_admin?, :is_moderator?, :is_guest?, :is_a?]

Note that #has_role is just another method and as such you can modify it to accept several parameters, an array, or other class. For example, we can make ‘has_role’ accept a list of roles.

class User
  attr_accessor :role

  def self.has_roles(*names)
    names.each do |name|
      define_method("is_#{name}?") do
        self.role == "#{name}"
      end
    end
  end

  has_roles :admin, :moderator, :guest
end