When to use alias_method_chain
CommentsRecently, I stumbled upon a fork of one of my projects in which someone made a really intelligent-sounding commit: “Remove needless indirection of alias_method_chain.” He used the term indirection, so I was duly impressed, and my curiosity was piqued: had I sinned against the programming gods and used an alias_method_chain when it wasn’t needed? No, I hadn’t. alias_method_chain is frequently abused in the Rails world, but it wouldn’t exist if there wasn’t a valid use case.
A Quick History Lesson
The debate over the proper usage of alias_method_chain is nothing new. In fact, if you google the term, you’ll come up with plenty of articles around the subject. Perhaps the best one, and the one that I think most well-intentioned people out to eradicate a_m_c from their code are inspired by, is this post from almost 2 years ago by Yehuda Katz, Rails core team member and one half of the superpowered coding entity known as Carlhuda. In this article, Yehuda explains that we shouldn’t be abusing alias_method_chain when inheritance and super will do the same job more effectively.
At this point, I think that a lot of people stop reading and promptly go about their code removing all calls to a_m_c, ignoring the interesting discussion about valid uses for the method in the comments. For most of them, this ends up working out just fine.
Method Resolution (it’s all about ancestry)
There are some cases, however, where inheritance and super can’t quite do the job, and they mainly revolve around cases where you don’t have control of the class whose behavior you’re trying to modify. This is frequently the case in Rails plugins, as you might imagine, as they frequently monkey-… I mean, freedom-patch Rails core classes, like ActiveRecord::Base. Then, things start to get a bit complicated. Whether or not you can get away with using super instead of alias_method_chain depends on where the method you’re trying to modify was originally defined. Was it defined In the class itself, or in a module that was included into the class?
Let’s look at a simple example, a Person class, that includes a Greetings module:
module Greetings
def hello
"Hello"
end
end
class Person
include Greetings
end
person = Person.new
person.hello # => "Hello"
So far, so good. Our Person can say hello. Now, let’s say we’d like to make our person be more neighborly:
module Flanderizer
def hello
"#{super}-diddly"
end
end
# FOR FREEDOM!!!
Person.send :include, Flanderizer
flanders = Person.new
flanders.hello # => "Hello-diddly"
This also works as we would expect. We’re able to call the previous implementation of our hello method and modify its output. But what if the situation were slightly different?
class Person
def hello
"Hello"
end
end
module Flanderizer
def hello
"#{super}-diddly"
end
end
# FOR FREEDOM!!!
Person.send :include, Flanderizer
flanders = Person.new
flanders.hello # => "Hello"
That wasn’t very neighborly, at all. What happened? Well, in Ruby, much like a monarchy, it’s all about ancestry.
When you call a method on an object, if that object’s class contains a matching method definition, the method resolution stops right there, and the object responds. Otherwise, the call continues up the object’s ancestor chain.
Creating the Family Tree
So, how does a class get a new ancestor?
The first way is obvious – inheritance. For instance, Person implicitly inherits from Object, so it gets all the methods defined on Objects automatically. If we had used class Person < MyClass in our class definition, then we’d have inherited the methods of the MyClass class, which itself would inherit from either Object or another superclass, and so on.
The second way is less obvious. Including a module into a class inserts that module as the most immediate ancestor of the class.
So, in the first case, our Person class included Greetings, then included Flanderizer, which gave us the following:
flanders.class.ancestors # => [Person, Flanderizer, Greetings, Object, Kernel]
Person didn’t have a hello method defined, so up the ancestry chain the method call went, encountering the hello method defined in the module Flanderizer, which called super, capturing the return value from the hello defined in the next ancestor, the Greetings module.
In the second example. we defined a hello method on the Person class. Our ancestry looked like…
flanders.class.ancestors # => [Person, Flanderizer, Object, Kernel]
…but our flanderized hello was never called, because Person responded to it directly. And it’s a good thing, too, as our call to super in Flanderizer would have been asking for the output of Object’s hello method, which won’t help us out much.
Enter alias_method_chain
What’s the solution? Well, we control the code in Person, Greetings, and Flanderizer, so the solution is to structure our code as it was in the first example, and use super. But let’s assume for a moment that we don’t control the source of the Person class. What if it’s gone and done something silly like defining the hello method, but we still want to make it more neighborly, anyway? Well, given what we’ve already discussed, the only route we can take to alter the behavior of a method already defined in the class is to:
- hop inside the offending class
- alias the original method to a new name
- make the original method name call our updated code
class Person
def hello
"Hello"
end
end
module Flanderizer
def self.included(base)
base.class_eval do
alias_method :hello_without_flanderizer, :hello
alias_method :hello, :hello_with_flanderizer
end
end
def hello_with_flanderizer
"#{hello_without_flanderizer}-diddly"
end
end
# FOR FREEDOM!!!
Person.send :include, Flanderizer
flanders = Person.new
flanders.hello # => "Hello-diddly"
flanders.class.ancestors # => [Person, Flanderizer, Object, Kernel]
And this is exactly what alias_method_chain encapsulates, which is why it’s just fine to use alias_method_chain in this case.
I hope this helps to clear up any confusion. I know that there have been a ton of posts on this subject, but since I still see this mistake made frequently by those with the best intentions, I thought it might help to write “just one more post on alias_method_chain”.
comments powered by Disqus