Using Ruby Metaprogramming to Increase READABILITY of Code

Let’s assume you have a class that has some sort of hash where it stores it’s data. We want to calculate amount that we need to pay, but only if number of items that we want to sell is entered. We’ll also assume that price is already present so we don’t need to check for that. It’s easy:

@store[:total] = @store[:price] * @store[:amount] if @store.has_key?(:amount)

A bit more readable would be:

set_total(price * amount) if amount_exists

This is possible by using method_missing that is cleverly embedded in Ruby method invocation chain. Ruby first tries to find method in calling class, then in it’s super classes and mixins. If all of that fails then it looks into method_missing. Basic usage is covered here. To make upper code possible, original example must be upgraded a little:

def method_missing(method, *params)
     method = method.to_s
     type = :get
     if (method.index('store_').nil? == false) #if you have name conflict use store_ prefix,
                                                             #or rename hash key or class method
         method.gsub!('store_','')
     elsif (method.index('_exists').nil? == false)
         method = method.gsub!('_exsists','')
         type = :check_if_exists
     elsif (method.index('set_').nil? == false && params.count == 1)
         method = method.gsub!('set_','')
         type = :set
     end
     method = method.to_sym
     if type == :get
         return @store[method] if @store.keys.collect(&:to_sym).include?(method)
     elsif type == :check_if_exists
        return @store.keys.collect(&:to_sym).include?(method)
     else
        return @store[method] = params[0]
     end
     return nil  # return nil if key does not exist
  end

The code is ugly and un-refactored, but it’ll work and it will make my class more user friendly. Hopefully I’ll never look again in method_missing method, so who cares how it looks… for now :) I care so I refactored it here.

Caveats :
1. Performance – obviously there is a performance penalty, I like not to dwell over it too much until it bites me. My experience has taught me that I am too stupid to catch performance issues (except the really obvious one) in advance. Knowing my limits I sit and wait for bottlenecks to introduce them self in a don’t-call-us-we’ll-call-you-manner (those guys really like to show off, don’t they)
2. Naming – if there is a hash item named same as existing method – you may not always get what you want. You can add prefix like this: store_price, store_amount… or you can just rename conflicting method
3. Encapsulation is broken – all of a sudden all hash elements are public even if a hash is a private member – on the bright side this can be fixed fairly easy

blog comments powered by Disqus