Safely overriding method_missing in a class that already has it

Today I’m making a ridiculous module. Pretty much all it does is override method_missing. There’s a big problem with that, though. What happens when the class you’re including the module in already has method_missing, and you don’t want to mess that up? After a few failed attempts, watching stack levels get too deep, and a little head-scratching, I made this:

module Hero

  def self.included(base)
    base.class_eval do
      unless method_defined? :method_missing
        def method_missing(meth, *args, &block); super; end
      end
      alias_method :old_method_missing, :method_missing
      alias_method :method_missing, :hero_method_missing
    end
  end

  def hero_method_missing(meth, *args, &block)
    if meth.to_s == 'hero'
      "Did I ever tell you you're my #{meth}?"
    else
      old_method_missing(meth, *args, &block)
    end
  end

end

There’s my trivial example. Hero is ready to be included in any class, method_missing defined or not. It’ll handle whenever you try to call a method named hero, and pass everything else on to whatever (the class’ original method_missing, or its parent’s method_missing). Now you can ruin ActiveRecord without totally ruining ActiveRecord!

The secret is defining method_missing in the class if it isn’t already, just super, so that the double alias_method is cool.

Take a look at it in action:

mvb:~ cms$ irb
001:0> load 'hero.rb'
true
002:0> class C;def method_missing(meth);"I'm fancy!";end;end
nil
003:0> C.new.hero
"I'm fancy!"
004:0> class C; include Hero; end
C
005:0> C.new.hero
"Did I ever tell you you're my hero?"
006:0> C.new.garbage!
"I'm fancy!"

I promise the actual use is more interesting, though possibly just as useless.

5 Comments »

  1. Dr Nic said,

    July 15, 2007 @ 10:10 pm

    Another thing you can try is using Modules. When you include a module in a class, it behaves a bit like superclasses - you can override methods but call super to access the version you overrode.

    I’m not sure which is preferred/ruby-fu-esque.

  2. Chris Shea said,

    July 15, 2007 @ 10:55 pm

    I’m not seeing what you’re seeing Dr. Nic:

    Given hero.rb containing:

    module Coward
      def to_i(mod)
        if mod == 2
          'hahaha'
        else
          super
        end
      end
    end

    I get:

    mvb:~ cms$ irb
    001:0> load 'hero.rb'
    true
    002:0> [1,2].to_i(2)
    NoMethodError: undefined method `to_i' for [1, 2]:Array
            from (irb):2
    003:0> class Array; include Coward; end
    Array
    004:0> [1,2].to_i(2)
    "hahaha"
    005:0> [1,2].to_i(3)
    NoMethodError: super: no superclass method `to_i'
            from ./hero.rb:6:in `to_i'
            from (irb):5
            from :0
    006:0> '1101'.to_i(2)
    13
    007:0> class String; include Coward; end
    String
    008:0> '1101'.to_i(2)
    13
  3. Dr Nic said,

    July 16, 2007 @ 5:13 am

    My example was more related to method_missing - http://pastie.caboo.se/79144

    But I was wrong.

    If a method is defined on the class, then you can only reimplement it with aliases and not modules.

    But if you include the module in a subclass of the class with the method to be overridden, then that’s fine - http://pastie.caboo.se/79145

    Doh - the last example doesn’t show the use of super

    Fixed here - http://pastie.caboo.se/79146

  4. Dr Nic said,

    July 16, 2007 @ 5:15 am

    PS. When you commented, it was the first time your name appears against this article. (its not in the about page either). Not sure if that’s deliberate or perhaps its worth adding your name to the sidebar + the article itself.

  5. ara.t.howard said,

    January 15, 2008 @ 1:35 pm

    cfp:~ > cat a.rb
    class A
     def foo
       p "A.foo"
     end
    end
    class B
    end
    
    module M
     NoGC = []
     def self.included other
       other.module_eval do
         if((foo = instance_method 'foo' rescue false))
           NoGC.push foo
           supra = "ObjectSpace._id2ref(#{ foo.object_id }).bind(self).call(*a, &b)"
         end
         eval  ruby a.rb
    "A.foo"
    "M.foo"
    "M.foo"

RSS feed for comments on this post · TrackBack URI

Leave a Comment