Module#prepend vs alias_method_chain
This article describes pros and cons of alias_method_chain
and its alternative approach.
The good (and bad) of alias_method_chain
When I first learned Rails, alias_method_chain totally blew my mind. It really showed off how flexible Ruby could be.
With just a single line of code, you could completely change the way a method worked. You no longer needed to hack around libraries to add the code you wanted, you could just add it on the fly. alias_method_chain
led to my first patches to gems, which led to my first pull requests, which led to my first open source contributions.
But just like monkey patching, alias_method_chain
got overused, and its problems started to become obvious:
The method names it generates are confusing, which makes errors hard to find and debug. For example:
class Person
def greeting
"Hello"
end
end
module GreetingWithExcitement
def self.included(base)
base.class_eval do
alias_method_chain :greeting, :excitement
end
end
def greeting_with_excitement
"#{greeting_without_excitement}!!!!!"
end
end
Person.send(:include, GreetingWithExcitement)
If you had an error in Person#greeting
, the backtrace would tell you that an error actually happened in Person#greeting_without_excitement
. But where is that method even defined? I don’t see it anywhere. How do you know which greeting method is the one with the bug in it? And the method names get even more confusing the more you chain.
- If you call alias_method_chain
twice with the same parameters on the same class, you could cause a stack overflow. (Can you see why?) This doesn’t normally happen, as long as your require
statements are consistent about which paths they use. But it’s super annoying if you frequently paste code into a Rails console.
- And the rest of the things described by Yehuda Katz’s blog post. This post convinced a lot of Rails devs to start abandoning alias_method_chain
in favor of module inheritance.
So, why is it still used?
You can replace most alias_method_chains
by overriding those methods in a module, and including those modules into your child classes. But that only works if you want to override your superclass, not your class itself. That is:
class ParentClass
def log
puts "In parent"
end
end
class ChildClass < ParentClass
def log
puts "In child"
super
end
def log_with_extra_message
puts "In child, with extra message"
log_without_extra_message
end
alias_method_chain :log, :extra_message
end
If you ran ChildClass.new.log
, you’d see:
In child, with extra message
In child
In parent
if you tried to use modules instead of alias_method_chain
, you could get the output to be:
In child
In child, with extra message
In parent
But you cannot match the original output without changing the log
method in ChildClass
. Ruby inheritance doesn’t work that way. Well, it didn’t.
What changed in Ruby 2.0?
Until Ruby 2.0, there was no way to add code below a class, only above it. But with prepend
, you can override a method in a class with a method from a module, and still access the class’s implementation with super
. So, using our last example, we could get the original output with:
class ParentClass
def log
puts "In parent"
end
end
module ExtraMessageLogging
def log
puts "In child, with extra message"
super
end
end
class ChildClass < ParentClass
prepend ExtraMessageLogging
def log
puts "In child"
super
end
end
In child, with extra message
In child
In parent
Perfect.
If prepend
is still hard to wrap your head around, think of it as doing something like this:
class NewChildClass < ChildClass
include ExtraMessageLogging
end
ChildClass = NewChildClass
Except it won’t mess with your class names, and it affects objects that already exist.
(Yes, you can reassign class names in Ruby. No, it’s probably not a great idea.)
What does this mean for Rails?
So, the last excuse for using alias_method_chain
is gone in Ruby 2.0. We could take one of the few remaining examples of alias_method_chain
in Rails:
# rails/activesupport/lib/active_support/core_ext/range/each.rb
require 'active_support/core_ext/module/aliasing'
class Range #:nodoc:
def each_with_time_with_zone(&block)
ensure_iteration_allowed
each_without_time_with_zone(&block)
end
alias_method_chain :each, :time_with_zone
def step_with_time_with_zone(n = 1, &block)
ensure_iteration_allowed
step_without_time_with_zone(n, &block)
end
alias_method_chain :step, :time_with_zone
private
def ensure_iteration_allowed
if first.is_a?(Time)
raise TypeError, "can't iterate from #{first.class}"
end
end
end
and replace it with a module, instead:
require 'active_support/core_ext/module/aliasing'
module RangeWithTimeWithZoneSupport #:nodoc:
def each(&block)
ensure_iteration_allowed
super(&block)
end
def step(n = 1, &block)
ensure_iteration_allowed
super(n, &block)
end
private
def ensure_iteration_allowed
if first.is_a?(Time)
raise TypeError, "can't iterate from #{first.class}"
end
end
end
Range.send(:prepend, RangeSupportingTimeWithZone)
It’s cleaner, Range#each
doesn’t get renamed, and ensure_iteration_allowed
isn’t monkey-patched in.
Use inheritance, not patches
Ruby gives you a ton of flexibility, and that’s one of the reasons I love it. But it also has a powerful object model. So when you want to inject your own code, try leaning on modules and inheritance before you hack it in. Your code will be much easier to understand and debug, and you’ll avoid some of the harder-to-detect side effects of something like alias_method_chain
.