Using Ruby's alias it is possible to reopen a class, override a method and still call the original.
class ClockRadioWhile 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
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
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 ClockRadioThe above version ensures that the correct version of
on = self.instance_method(:on!)
define_method(:on!) do
on.bind(self).call
@display_time = true
end
def display_time?
@display_time
end
end
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
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.
ReplyDeleteWhy 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
I just read the original blog posting you'd linked to: http://split-s.blogspot.com/2006/01/replacing-methods.html
ReplyDeleteThere 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.)
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.
ReplyDeleteThanks again, I hope the new example is a bit more clear.
Thanks, great article and awesome point!!
ReplyDeleteThanks 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.
ReplyDeleteSo 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.
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.
ReplyDelete- 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
define_method is slow as fuck.
ReplyDeleteHi Jay, this is an old post of yours (and I was a Ruby newbie when I first read it and commented on it).
ReplyDeleteIn 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.
I came to know what is alias in ruby and thanks for that.
ReplyDeleteThree 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.
ReplyDeleteThe 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.
Thank you. This is great!
ReplyDeleteJay, I linked to your post from https://github.com/lsiden/overider.
ReplyDeleteThank you!