undef_method != remove_method
When trying to remove behavior from your classes or objects, you will quickly
stumble upon undef_method
and remove_method
. Even when your are just trying
to change an implementation you will need one of these. Otherwise Ruby will warn
you, that you are discarding an older code chunk.
So let’s quickly examine the basic difference between the two. The RDoc of
undef_method
already
gives good starting point.
Prevents the current class from responding to calls to the named method. Contrast this with remove_method, which deletes the method from the particular class; Ruby will still search superclasses and mixed-in modules for a possible receiver.
There is also some sample code, but mine will be better. (No, it will not, but have you ever heard of NIH?) And I will use two test techniques promoted by Jay Fields to make sure, your mind is still able to move a bit. These are dust and anonymous classes for tests. Not too much magic for the rest of us, but both make posting test code a lot easier, quicker and more readable.
I will post it in chunks. At the end of the article you will have a complete ruby test.
require "rubygems"
require "dust"
No magic. Just wait a bit more…
unit_tests do
This is dust
’s way to say class FooTest < Test::Unit::TestCase
. I really
like dust
.
def setup
@base_class = Class.new do
def method
"answered in base class"
end
end
@sub_class = Class.new(@base_class) do
def method
"answered in sub class"
end
end
@base_instance = @base_class.new
@sub_instance = @sub_class.new
end
The setup method is pretty basic. We are creating a fresh inheritance hierarchy
for every test method. This will simply prevent undesired side effects. Changes
to classes are so global. Okay, the @base_class
– actually it has no name,
but anyways – has a single method method
. @sub_class
inherits from
@base_class
– now I get the naming schema – and defines its own version of
the method
method.
For each run, I also instantiate an instance for each class to play with.
test "Basic setup works" do
assert_equal @base_instance.method, "answered in base class"
assert_equal @sub_instance.method, "answered in sub class"
end
This is dust
’s way to say def test_basic_setup_works; ...; end
. How do you
fell?
The code itself is really basic. I’m just making sure, that the setup works and nobody changed my tests significantly.
remove_method
Let’s start with the well-behaving twin: remove_method
.
test "remove_method let's you undo method definitions" do
@sub_class.send(:remove_method, :method)
assert_equal @base_instance.method, "answered in base class"
assert_equal @sub_instance.method, "answered in base class"
end
I’m using send
here to circumvent the visibility restrictions. Don’t do this
at home. Use class_eval
instead. Your children (using 1.9) will be happier.
After removing a method, its definition is removed from the class itself. When you try to access it, the correct implementation will be searched within the ancestors. This way you can get rid of your customizations and activate a more general definition. This is mainly the thing you would want to use, before redefining a method, to get rid of the warning.
There is not much else to say about it. Sorry.
undef_method
The less it sounds like plain english, the hackier it is (ref. awk vs. grep).
undef_method
does not only remove the definition from the current class, but
causes Exceptions. Let’s have a look at the tests first.
test "undef_method causes NoMethodErrors" do
@sub_class.send(:undef_method, :method)
assert_raise(NoMethodError) { @sub_instance.method }
end
Although, there is a definition in the base class, it is not used. Instead, we
get an error. Okay, but this might make you think, that now, the method method
is totally gone …
test "undef_method does not affect superclasses" do
@sub_class.send(:undef_method, :method)
assert_equal @base_instance.method, "answered in base class"
end
… no it is not. It is still there – in the base class. undef_method
actually hides all implementations in superclasses. This somehow breaks the
inheritance relation. Although sub inherits from base and although base has an
implementation and sub does not, the happy programmer caused a NoMethodError.
The following test shall stress this reasoning.
test "undef_method breaks inheritance" do
@sub_class.send(:undef_method, :method)
assert @base_class.instance_methods.include?("method")
assert @sub_instance.kind_of?(@base_class)
# This is the point where you should say: No!
assert !@sub_instance.respond_to?(:method)
end
We are still missing an
end
here. The test is done, it will work on all 1.8 compatible machines.
Strange summary
More or less – without the hooks to update the functionality of respond_to
,
instance_methods
and their friends – remove_method
and undef_method
could
be implemented like the following
class Module
def remove_method(name)
define_method(name) do
super
end
end
def undef_method(name)
define_method(name) do
raise NoMethodError,
"undefined method `#{name}' for #{self}",
caller(1)
end
end
end
The one passes control flow to an implementation somewhere else, the other
raises Exceptions. Try it, think about it, and tell me, why one needs
undef_method
.
My name is Gregor Schmidt. I am a freelance Ruby and JavaScript web developer based in Berlin, Germany. I do Ruby and Rails since 2005, JavaScript since 2006. I wrote my first Redmine plugin in 2007.
I mainly work with Rails, Backbone, and Bootstrap, but I am also good at picking up new frameworks, since I will probably know most of their concepts from other projects.
If your interested in more of my previous work have a look at my portfolio. I have also published my rates for everybody to see. I would love to hear, how I may help you.