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:
And I had a subclass that attempted to call that before_action
class method:
Then filter_decrufter could define a singleton method that would check the before_action
arguments and flag any options for missing actions:
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:
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:
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":
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:
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.