Class methods are methods on a object's singleton class. Everyone knows this (1). I think I sort of knew it also, but recently I was working on a thing and this was brought home to me.
I was working on integration tests for filter_decrufter, so I wanted to define a sort of stubbed out ActionController::Base
(2) with a class method that simulated defining a before action:
module ActionController
class Base
def self.before_action(*args)
end
end
end
And I had a subclass that attempted to call that before_action
class method:
class AnotherFakeController < ApplicationController
before_action :foo, :only => [:bar]
def foo
end
def self.my_action_methods
[:foo]
end
end
Then filter_decrufter could define a singleton method that would check the before_action
arguments and flag any options for missing actions:
# in a loop where filter_sym is before_action, after_action, around_filter, etc
ActionController::Base.define_singleton_method(filter_sym) do |*args, &blk|
# ... gather some data about *args ...
super(*args, &blk)
end
What I was seeing, though, was that AnotherFakeController
would raise an exception when I loaded it and it attempted to call the parent class method as part of the class definition:
1) Error:
FilterDecrufterTest#test_finds_problems:
NoMethodError: super: no superclass method `before_action' for AnotherFakeController:Class
/lib/filter_decrufter/checker.rb:100:in `block in patch_method'
/test/integration/another_fake_controller.rb:3:in `<class:AnotherFakeController>'
But why? The before_action
method is declared right there in ActionController::Base
!
The problem was that the before_action
method that ActionController::Base
defined was living on ActionController::Base
's singleton class. No need to take my word for it though; you can verify this by defining a class method and checking the singleton methods:
irb(main):001:0> class Foo ; def self.bar ; puts "Foo#bar" ; end ; end
=> :bar
irb(main):002:0> Foo.singleton_methods
=> [:bar]
So when I defined a singleton method on ActionController::Base
I was not intercepting the method call like I intended. Instead, I was redefining the existing method. And my new method definition called super
, but since I'd redefined the only method with that name in this class's ancestor chain, there was no superclass method by that name available, and so bam, exception.
As a side note, singleton_methods
looks up the inheritance chain, so it's not quite reliable for saying "this method is defined right here":
irb(main):001:0> class Foo ; def self.bar ; end ; end
=> :bar
irb(main):002:0> class Buz < Foo ; end ; class Biz < Buz ; end
=> nil
irb(main):003:0> Biz.singleton_methods
=> [:bar]
Back to the original problem - how to solve it? By defining the method not on the singleton class but instead further up the ancestor chain. And how to do that? By defining the method in a module and extend
'ing that module:
# Define a method with our method in it
irb(main):001:0> module Buz ; def bar ; puts "Buz#bar" ; end ; end
=> :bar
# extend that module so that it's a class method
# rather than include'ing which would make it an instance method
irb(main):002:0> class Foo ; extend Buz ; end
=> Foo
# now define a singleton method that will intercept invocations of that method
irb(main):003:0> Foo.define_singleton_method(:bar) { puts "Foo#bar" ; super() }
=> :bar
# and demonstrate the that interceptor gets called first and then calls the superclass
irb(main):004:0> Foo.bar
Foo#bar
Buz#bar
I think the lessons learned are the usual ones. Unexpected exceptions are an opportunity for learning something. Don't confuse Java static methods with Ruby's class methods. Verify expected behavior in irb or in a small program. And read books written by people who have poured a lot of time and energy into the topic that's currently giving you trouble!
(1) Because everyone's read Paolo Perrotta's excellent Metaprogramming Ruby 2nd Ed.
(2) You could argue that I should just declare a dependency on actionpack and use it. That probably would be better; I might do that.