Ghost methods and meta programming can make your life much much much easier

I have been cleaning up some of my codes today at my a ruby on rails app, I had 10 similar with minor difference methods which kept me thinking why would I write 10 separate methods rather than 1 using ghost methods of ruby? But things are a little bit different when we work on rails that is basically the motivation behind this blog.

Suppose my client started to take orders from its customer to build a custom car, so imagine how many of this following methods we will need to write!!

def add_tire
    @current_build = get_current_build
    Tire = Tire.find(params[:tire_id])
    # todo do stuff
    redirect_to current_build_url
end

def add_engine
    @current_build = get_current_build
    engine = Engine.find(params[:engine_id])
    # todo add methods
    redirect_to current_build_url
end

#....more similar looking add_items methods

Probably a hundred or even the method can reach thousand. But don’t surprise, ruby has its own elegant way to handle this type of repeating. Using ruby support of ghost methods, we can solve this problem. Usually a ghost methods are being handled using method_missing, if you already are not aware of the power of method_missing, then let me tell you in short, method_missing is a method that is being called when a method is being called but it is not defined in the object. So basically when a method is being called in check within it, if not found it checks its parent if that method exists there or not, if not found then it checks its parent too, this check goes all the way to ruby object class as object is the parent of all ruby class.

Now let me show you an example:

class Test
    def say_hi(to)
        "Saying hi to "+to
    end
    def method_missing(name, *args)
        say_hi(*args)
    end
end

irb(main):022:0> Test.new.say_hi("mike")
=> "saying hi to mike"
irb(main):023:0> Test.new.say_hello("mike")
=> "saying hi to mike"

 

 

This hello, or hi method does not exist in Test class, but they are acting as if they exist, thats why they are called ghost methods.

Every ruby class has a send method, that takes method name as string argument and capable to run that method.

class Test
    def say_hi(to)
        "Saying hi to "+to
    end
    def say_bye(to)
        "Saying bye to "+to
    end
    def method_missing(method, *args)
        self.send("#{method}",*args)
    end
end

irb(main):023:0> Test.new.say_hi("mike")
=> "saying hi to mike"
irb(main):022:0> Test.new.hi("mike")
=> "saying hi to mike"
irb(main):023:0> Test.new.say_bye("mike")
=> "saying bye to mike"
irb(main):022:0> Test.new.bye("mike")
=> "saying bye to mike"

 

 

We have seen how to call a method dynamically, but we may also need to call another class name dynamically, for that Object.const_get takes string as parameter and call that class. (Object.const_get "Test").new.send("hi","mike") is the same as Test.new.hi("mike").

irb(main):023:0> (Object.const_get "Test").new.send("hi","mike")
=> "saying hi to mike"

 

In ruby meta programming there is a method called eval which exicutes a string as ruby code.

irb(main):025:0> eval("Test.new.hi('mike')")
=> "saying hi to mike"

 

Now I think we know everything to attempt to write our ghost method at controller, but writing ghost method with method_missing does not help much in rails controller because rails does not always require methods to be present in controller if corresponding template exists. So when action is missing it starts to attempt rendering templates, thats why method_missing does not invokes. But fear not, we have action_missing which is being called when ruby failed to find action. Here is an example of our car order case:

  def action_missing(m, *args, &block)
    if m.starts_with? "add_"
      k=(m.split "_", 2) [1]

      @current_build = get_current_build
      qty=params[:qty]      

      build= (Object.const_get "#{k.camelize}Build").create({(k+"_id").to_sym => params[(k+"_id").to_sym], :market_status_id => params[:market_status]})
      eval("@current_build.#{k}_builds << build")
      redirect_to current_build_url
    else
      super
    end
  end

Here we go, we have saved at least 100 lines of code, not only that, now we need to change only one line of code on change of methods, where previously, we had to change 100 lines on change.