Sunday, December 24, 2006

Ruby: Alias method alternative

Originally blogged by Martin @ split-s. I'm reposting because I don't see this method used very often.

Using Ruby's alias it is possible to reopen a class, override a method and still call the original.
class ClockRadio
def on!
@on = true
end

def on?
@on
end
end

class ClockRadio
alias :old_on! :on!

def on!
old_on!
@display_time = true
end

def display_time?
@display_time
end
end
While this works, it can cause unexpected results and leave around artifacts. In isolation this doesn't look risky; however, in a large codebase someone could easily define old_on! or use it as their alias name also. The other, much smaller, issue is that old_on! will be left as a method on ClockRadio when you actually have no desire to expose this method outside of calling it from on!.

An alternative is to capture the on! method as an unbound method, bind it to the current instance, and call it explicitly.
class ClockRadio
on = self.instance_method(:on!)

define_method(:on!) do
on.bind(self).call
@display_time = true
end

def display_time?
@display_time
end
end
The above version ensures that the correct version of on! will be called from the on! implementation defined in the reopened version ClockRadio. This version also lets the reference to the old_on! fall out of scope after the class is defined; therefore, there are no additional methods left around as side effects.

Below are the tests that prove the concept.
class AliasMethodAlternativeTest < Test::Unit::TestCase

def test_aliased_method_returns_true_for_on
radio = ClockRadio.new
radio.on!
assert_equal true, radio.on?
end

def test_aliased_method_returns_true_for_display_time
radio = ClockRadio.new
radio.on!
assert_equal true, radio.display_time?
end

end

12 comments:

  1. Umm... I kinda new to Ruby, so forgive me if this sounds silly -- I didn't quite get what you are trying to achieve here.

    Why can't we simply use 'super' to call the method we're overriding?

    Eg-

    class ClockRadio < Radio
    def on!
    super
    @display_time = true
    end

    def display_time?
    @display_time
    end
    end

    ReplyDelete
  2. I just read the original blog posting you'd linked to: http://split-s.blogspot.com/2006/01/replacing-methods.html

    There Martin is actually talking about the situation where you are replacing a method in the same class. So of course, 'super' won't work there and you need this technique. :-)

    (Btw, I'm regular reader of your blog, Jay. Thanks for keeping at it with the frequency that you do. I'm learning a lot from you.)

    ReplyDelete
  3. Aman, good point, thanks. I've updated the example to use a (hopefully) more realistic example. I find I use this technique fairly often when testing and I want to change the behavior temporarily. I decided not to use that as the example since Stubba and Mocha almost remove the need, but it's still a good trick to know.

    Thanks again, I hope the new example is a bit more clear.

    ReplyDelete
  4. Thanks, great article and awesome point!!

    ReplyDelete
  5. Thanks for a nice example. A point worth noting is that the instance_method does not "detach" or unbind the method from the original class; all we get is a copy of the method (strictly speaking we get an object of type UnboundMethod) which is not bound to any object.

    So if we were to comment out the define_method call, we could still do
    c = ClockRadio.new
    c.on! # returns true
    while still retaining the statement
    on = self.instance_method(:on!)
    in the reopened ClockRadio class.

    Also, one cannot "get" an instance_method from one class and bind it to an object of another class, unless this other class derives from the original. This is because the "Unbound"Method remembers where it came from and retains affinity to that class.

    ReplyDelete
  6. goodieboy10:04 PM

    Perfect! Man Ruby is so great. I'm using acts_as_versioned, and I didn't want to default to incrementing the version number when saving. I wanted that to be explicit. I'm also using ResourceController, which always calls "save". So this is what I came up with and it does exactly what I want. Thank you for this.
    - Matt


    # "copy" the old save method, with automatically increments the version
    _save_with_revision = self.instance_method(:save)

    # create a new method called save_with_revision,
    # that calls the original save method
    define_method(:save_with_revision) do
    # bind self for scope
    _save_with_revision.bind(self).call
    end

    # now make the save method, call the save_without_revision
    def save(*args)
    save_without_revision(*args)
    end

    ReplyDelete
  7. Anonymous1:38 AM

    define_method is slow as fuck.

    ReplyDelete
  8. Hi Jay, this is an old post of yours (and I was a Ruby newbie when I first read it and commented on it).

    In a comment, you said, "I find I use this technique fairly often when testing and I want to change the behavior temporarily. I decided not to use that as the example since Stubba and Mocha almost remove the need, but it's still a good trick to know."

    Hmm... could it be, that after all this while, I've run into test code written by you in a Ruby project at ThoughtWorks? :-) Anyways...

    The problem with reopening a class to stub out methods is that even though it'll do the trick for that particular test case, other tests will fail when run as a suite via rake. This is because once a class is reopened by one test file that is loaded by rake, the original implementation is not reverted back to for the other tests which probably weren't expecting the stubbed out version. Those other tests will fail (although they'd pass when run in isolation themselves... leading to a lot of confusion). I've written more this problem here.

    ReplyDelete
  9. I came to know what is alias in ruby and thanks for that.

    ReplyDelete
  10. Three years later and your blog provided the key to saving my ass on my project approaching deadline. A big THANK YOU your research and your writing. I'm referring to the instance_method ... bind ... call technique.

    The other bloggers talk about using alias to hang on to the old method. You talk about it too. That used to work but not anymore.

    ReplyDelete
  11. Thank you. This is great!

    ReplyDelete
  12. Jay, I linked to your post from https://github.com/lsiden/overider.

    Thank you!

    ReplyDelete

Note: Only a member of this blog may post a comment.