ActiveRecord method_missing with multiple inheritance
Recently I ran into a case where I had two separate “acts_as” style ActiveRecord model extensions which both abused respond_to?
and method_missing
to dynamically provide virtual attribute support.
The first thought I had was that won’t the two included method_missing definitions shadow each other? And which one will get invoked as the default one?
Well as it turns out then including a module into some class wont actually overwrite the methods, but instead creates local proxies for the included modules.
And when several definitions with the same name are found, the last included method will be invoked unless the method definition exists in the class (where the modules are included).
Illustrated in code:
def foo
puts "foo"
end
end
module Bar1
def foo
puts "bar1"
super
end
end
module Bar2
def foo
puts "bar2"
super
end
end
Foo.send(:include, Bar1)
Foo.send(:include, Bar2)
Foo.new.foo => "foo"
Now when you remove the original definition from Foo
class…
end
Foo.send(:include, Bar1)
Foo.send(:include, Bar2)
Foo.new.foo => "bar2"
Why it works like this? Because when you include modules into a class, the modules will end up as ancestors for the class.
=> [Foo, Bar2, Bar1, Object, Kernel]
This means that when invoking a method, the class Foo
is checked first. When no definition is found, the ancestors are invoked for the same method from the last included one to the first.
In our case Bar2
module will be asked for the foo
method after its not found in the Foo
class.
Something actually useful in the real world
As you might already know, ActiveRecord::Base
defines a method_missing
for all Rails models.
Now when i define my own method_missing
implementations, i want to be sure that my methods will get invoked in conjunction with the default one.
My first thought was “heeeey… when ActiveRecord already defines method_missing
, wont my methods get shadowed and ignored?”. Luckily, this is not the case at all.
ActiveRecord::Base.method_missing
does its own little magic (like looking up attributes etc.) and when nothing is found, super
is called.
As explained earlier – included modules end up as ancestors of the current class, meaning calling super
inside ActiveRecord::Base.method_missing
, it ends up calling the last included module definition of method_missing
.
Queue up the metaprogramming hat!
end
module Foo
def method_missing(method, *args, &block)
puts "#{method} invoked in Foo"
if method.to_s == "foo" or method.to_s == "foo="
# do my magic
else
super
end
end
end
module Bar
def method_missing(method, *args, &block)
puts "#{method} invoked in Bar"
if method.to_s == "bar" or method.to_s == "bar="
# do my magic
else
super
end
end
end
FooBar.send(:include, Foo)
FooBar.send(:include, Bar)
>> FooBar.new.method_missing("foo")
foo invoked in Bar
foo invoked in Foo
>> FooBar.new.method_missing("bar")
bar invoked in Bar
>> FooBar.new.method_missing(:lol)
lol invoked in Bar
lol invoked in Foo
NoMethodError: undefined method `lol' for #<foobar :0x2260640>
from /lib/active_record/attribute_methods.rb:260:in `method_missing'
from (irb):12:in `method_missing'
from (irb):24:in `method_missing'
from (irb):39
The above example is basically intercepting any calls to method_missing
in modules Foo
and Bar
, which in turn
process the arguments and decide whether to do some dark magic or pass the invocation onwards in the call chain, i.e the next included module.
More elaborate explanation
The difference in the two above examples is that in the first example, the foo
method was called on the class itself, rather than its included modules.
In the second example the included modules method_missing
methods were called first, the invocation was passed through and it finally ended up in ActiveRecord::Base.method_missing
.
Kind of looks like it’s all backwards in the second example – FooBar
is an ActiveRecord::Base
object, so why wasn’t it’s definition of method_missing
called first?
Thats’s because FooBar
itself does not define this method, it’s actually included via ActiveRecord::Base
from ActiveRecord::AttributeMethods
. And this inclusion happens earlier than our module inclusions, meaning its higher up in the ancestors chain.
As modules included lower in the ancestors chain get invoked first, it’s perfectly clear why it works as it does.
Example of the call chain order:
2. Bar.method_missing(:lol) # exists, the invocation is passed through
3. Foo.method_missing(:lol) # exists, the invocation is passed through
4. ActiveRecord::AttributeMethods # exists, no attribute is found, passed through
5. Kernel.method_missing(:lol) # ends up here, no method named "lol" is found, exception thrown
All of this also applies to ActiveRecord::Base.responds_to?
so you can apply the same logic while working with this method.
Using the information above you can easily write your model extensions, using magic methods like these and without having to worry too much about other parallel extensions.
Sure, there can be cases where two separate extension intercept the same method calls, but that’s a whole another issue.
How to: facepalm propely
Actually the first solution for my problem was using alias_method_chain.
It looked something like this:
def self.included(base)
base.send(:include, InstanceMethods)
end
module InstanceMethods
def method_missing_with_foo(method, *args, &block)
if method.to_s == "foo" or method.to_s == "foo="
# do my magic
else
method_missing_without_foo(method, *args, &block)
end
end
alias_method_chain :method_missing, :foo
end
end
module Bar
def self.included(base)
base.send(:include, InstanceMethods)
end
module InstanceMethods
def method_missing_with_bar(method, *args, &block)
if method.to_s == "bar" or method.to_s == "bar="
# do my magic
else
method_missing_without_bar(method, *args, &block)
end
end
alias_method_chain :method_missing, :bar
end
end
No matter what attribute name, virtual or real, i tested this code with, i always ended up with false
as the return value.
After debugging and testing the code for several hours with my co-worker, it hit us both at the same second – the alias_method_chain calls get evaluated in the module at runtime, which runs them in Module scope, not the ActiveRecord::Base scope. No wonder it returned false all the time.
This is what it should look like:
def self.included(base)
base.send(:include, InstanceMethods)
alias_method_chain :method_missing, :bar
end
module InstanceMethods
def method_missing_with_bar(method, *args, &block)
if method.to_s == "bar" or method.to_s == "bar="
# do my magic
else
method_missing_without_bar(method, *args, &block)
end
end
end
end
After several facepalms and “dohs” this solution actually led me to research on how and in what order are modules included and how the ancestors hierarchy will end up looking like.
That, in turn, led me to writing a better, cleaner solution for my problem and this long, hopefully useful blog post.
Conclusion: bugs are good :)
Comments are closed here.