Posted on Aug 29, 2018 [ 10 min read ]

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.

X
- +
B
A - Z
Copyleft 2024 Gritwerkz.

Twitter Email

Back to Top