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

  

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s