The Rise and Fall of alias_method_chain - Metaprogramming in Rails - Metaprogramming Ruby 2: Program Like the Ruby Pros (2014)

Metaprogramming Ruby 2: Program Like the Ruby Pros (2014)

Part 2. Metaprogramming in Rails

Chapter 11. The Rise and Fall of alias_method_chain

In the previous two chapters, we looked at the modular design of Rails and how that design changed over time. Now I’ll tell you of a more dramatic change in Rails’ history: how a method named alias_method_chain rose to fame, fell in disgrace, and was eventually scrapped almost entirely from the Rails codebase.

The Rise of alias_method_chain

In The Include-and-Extend Trick, I showed you a snippet of code from an old version of Rails…minus a few interesting lines. Here is the same code again, with those lines now visible and marked with arrows:

gems/activerecord-2.3.2/lib/active_record/validations.rb

module ActiveRecord

module Validations

def self.included(base)

base.extend ClassMethods

*

base.class_eval do

*

alias_method_chain :save, :validation

*

alias_method_chain :save!, :validation

*

end

# ...

end

When ActiveRecord::Base includes the Validations module, the marked lines reopen Base and call a method named alias_method_chain. Let me show you a quick example to explain what alias_method_chain does.

The Reason for alias_method_chain

Suppose you have a module that defines a greet method. It might look like the following code.

part2/greet_with_aliases.rb

module Greetings

def greet

"hello"

end

end

class MyClass

include Greetings

end

MyClass.new.greet # => "hello"

Now suppose you want to wrap optional functionality around greet—for example, you want your greetings to be a bit more enthusiastic. You can do that with a couple of Around Aliases (Around Alias):

class MyClass

include Greetings

def greet_with_enthusiasm

"Hey, #{greet_without_enthusiasm}!"

end

alias_method :greet_without_enthusiasm, :greet

alias_method :greet, :greet_with_enthusiasm

end

MyClass.new.greet # => "Hey, hello!"

I defined two new methods: greet_without_enthusiasm and greet_with_enthusiasm. The first method is just an alias of the original greet. The second method calls the first method and also wraps some happiness around it. I also aliased greet to the new enthusiastic method—so the callers ofgreet will get the enthusiastic behavior by default, unless they explicitly avoid it by calling greet_without_enthusiasm instead:

MyClass.new.greet_without_enthusiasm # => "hello"

To sum it all up, the original greet is now called greet_without_enthusiasm. If you want the enthusiastic behavior, you can call either greet_with_enthusiasm or greet, which are actually aliases of the same method.

This idea of wrapping a new feature around an existing method is common in Rails. In all cases, you end up with three methods that follow the naming conventions I just showed you: method, method_with_feature, and method_without_feature. The only the first two methods include the new feature.

Instead of duplicating these aliases all over the place, Rails provided a metaprogramming method that did it all for you. It was named Module#alias_method_chain, and it was part of the Active Support library. I’m saying “it was” rather than “it is” for reasons that will be clear soon—but if you look inside Active Support, you’ll find alias_method_chain is still there. Let’s look at it.

Inside alias_method_chain

Here is the code of alias_method_chain:

gems/activesupport-4.1.0/lib/active_support/core_ext/module/aliasing.rb

class Module

def alias_method_chain(target, feature)

# Strip out punctuation on predicates or bang methods since

# e.g. target?_without_feature is not a valid method name.

aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1

yield(aliased_target, punctuation) if block_given?

with_method = "#{aliased_target}_with_#{feature}#{punctuation}"

without_method = "#{aliased_target}_without_#{feature}#{punctuation}"

alias_method without_method, target

alias_method target, with_method

case

when public_method_defined?(without_method)

public target

when protected_method_defined?(without_method)

protected target

when private_method_defined?(without_method)

private target

end

end

end

alias_method_chain takes the name of a target method and the name of an additional feature. From those two, it calculates the name of two new methods: target_without_feature and target_with_feature. Then it stores away the original target as target_without_feature, and it aliasestarget_with_feature to target (assuming that a method called target_with_feature is defined somewhere in the same module). Finally, the case switch sets the visibility of target_without_feature so that it’s the same visibility as the original target.

alias_method_chain also has a few more features, such as yielding to a block so that the caller can override the default naming, and dealing with methods that end with an exclamation or a question mark—but essentially, it just builds an Around Alias (Around Alias). Let’s see how this mechanism was used in ActiveRecord::Validations.

One Last Look at Validations

Here is the code from the old version of ActiveRecord::Validations again:

def self.included(base)

base.extend ClassMethods

# ...

base.class_eval do

alias_method_chain :save, :validation

alias_method_chain :save!, :validation

end

# ...

end

These lines reopen the ActiveRecord::Base class and hack its save and save! methods to add validation. This aliasing ensures that you will get automatic validation whenever you save an object to the database. If you want to save without validating, you can call the aliased versions of the original method, now called save_without_validation.

For the entire scheme to work, the Validations module still needs to define two methods named save_with_validation and save_with_validation!:

gems/activerecord-2.3.2/lib/active_record/validations.rb

module ActiveRecord

module Validations

def save_with_validation(perform_validation = true)

if perform_validation && valid? || !perform_validation

save_without_validation

else

false

end

end

def save_with_validation!

if valid?

save_without_validation!

else

raise RecordInvalid.new(self)

end

end

def valid?

# ...

The actual validation happens in the valid? method. Validation#save_with_validation returns false if the validation fails, or if the caller explicitly disables validation. Otherwise, it just calls the original save_without_validation. Validation#save_with_validation! raises an exception if the validation fails, and otherwise falls back to the original save_with_validation!.

This is how alias_method_chain was used around the times of Rails 2. Things have changed since then, as I will explain next.

The Fall of alias_method_chain

In the previous two chapters, you’ve seen that the libraries in Rails are mostly built by assembling modules. Back in Rails 2, many of those modules used alias_method_chain to wrap functionality around the methods of their includers. The authors of libraries that extended Rails adopted the same mechanism to wrap their own functionality around the Rails methods. As a result, alias_method_chain was used all over the place, both in Rails and in dozens of third-party libraries.

alias_method_chain was good at removing duplicated aliases, but it also came with a few problems of its own. For a start, alias_method_chain is just an encapsulation of an Around Alias (Around Alias), and Around Aliases have the subtle problems that you might remember from The Thor Example. To make things worse, alias_method_chain turned out to be too clever for its own good: with all the method renaming and shuffling that was going on in Rails, it could become hard to track which version of a method you were actually calling.

However, the most damning issue of alias_method_chain was that it was simply unnecessary in most cases. Ruby is an object-oriented language, so it provides a more elegant, built-in way of wrapping functionality around an existing method. Think back to our example of adding enthusiasm to the greet method:

part2/greet_with_super.rb

module Greetings

def greet

"hello"

end

end

class MyClass

include Greetings

end

MyClass.new.greet # => "hello"

Instead of using aliases to wrap additional functionality around greet, you can just redefine greet in a separate module and include that module instead:

part2/greet_with_super.rb

module EnthusiasticGreetings

include Greetings

def greet

"Hey, #{super}!"

end

end

class MyClass

include EnthusiasticGreetings

end

MyClass.ancestors[0..2] # => [MyClass, EnthusiasticGreetings, Greetings]

MyClass.new.greet # => "Hey, hello!"

The chain of ancestors of MyClass includes EnthusiasticGreetings and then Greetings, in that order. That’s why by calling greet, you end up calling EnthusiasticGreetings#greet, and EnthusiasticGreetings#greet can in turn call into Greetings#greet with super. This solution is not as glamorous as alias_method_chain, but it’s simpler and all the better for it. Recent versions of ActiveRecord::Validations acknowledge that simplicity by using a regular override instead of alias_method_chain:

gems/activerecord-4.1.0/lib/active_record/validations.rb

module ActiveRecord

module Validations

# The validation process on save can be skipped by passing

# <tt>validate: false</tt>.

# The regular Base#save method is replaced with this when the

# validations module is mixed in, which it is by default.

def save(options={})

perform_validations(options) ? super : false

end

# Attempts to save the record just like Base#save but will raise

# a +RecordInvalid+ exception instead of returning +false+ if

# the record is not valid.

def save!(options={})

perform_validations(options) ? super : raise(RecordInvalid.new(self))

end

def perform_validations(options={})

# ...

Validation#save performs the actual validation (by calling the private method perform_validations). If the validation succeeds, then it proceeds with the normal save code in ActiveRecord::Base by calling super. If the validation fails, then it returns false. Validation#save! follows the same steps, except that it raises an exception if the validation fails.

These days, Rails barely ever uses alias_method_chain. You can still find this method called inside Active Support and some third-party libraries, but there is no trace of it in libraries such as Active Record. The once-popular alias_method_chain has nearly disappeared from the Rails environment.

However, there is still one case where you might argue that alias_method_chain works better than its object-oriented alternative. Let’s look at it.

The Birth of Module#prepend

Let’s add a twist to our ongoing greet method example: instead of defining greet in a module, let’s assume it’s defined directly in the class.

part2/greet_with_prepend.rb

class MyClass

def greet

"hello"

end

end

MyClass.new.greet # => "hello"

In this case, you cannot wrap functionality around greet by simply including a module that overrides it:

part2/greet_with_prepend_broken.rb

module EnthusiasticGreetings

def greet

"Hey, #{super}!"

end

end

class MyClass

include EnthusiasticGreetings

end

MyClass.ancestors[0..2] # => [MyClass, EnthusiasticGreetings, Object]

MyClass.new.greet # => "hello"

The code above shows that when you include EnthusiasticGreetings, that module gets higher than the class in the class’s chain of ancestors. As a result, the greet method in the class overrides the greet method in the module, instead of the other way around.

You could solve this problem by extracting greet from MyClass into its own module, like the Greetings module in the previous section. If you do that, you’ll be able to insert an intermediary module like EnthusiasticGreetings in the chain and use the override-and-call-super technique, just as we did back then. However, you might be unable to do that—for example, because MyClass is part of a library such as Rails, and you’re extending that library rather than working directly on its source code. This limitation is the main reason why many Rubyists still use alias_method_chainwhen they extend Rails.

However, Ruby 2.0 came with an elegant solution for this problem in the form of Module#prepend:

module EnthusiasticGreetings

def greet

"Hey, #{super}!"

end

end

class MyClass

prepend EnthusiasticGreetings

end

MyClass.ancestors[0..2] # => [EnthusiasticGreetings, MyClass, Object]

MyClass.new.greet # => "Hey, hello!"

This is a Prepended Wrapper (Prepended Wrapper), a modern alternative to Around Aliases (Around Alias). Because we used prepend, the EnthusiasticGreetings#greet got lower than MyClass#greet in MyClass’s chain of ancestors, so we went back to the usual trick of overriding greet and calling super.

As I write, Rails is not using Module#prepend yet, because it’s still aiming to be compatible with Ruby 1.9. When Rails eventually drops this constraint, I expect that prepend will make its appearance in Rails and its extensions. At that point, there will be no urgent reason to callalias_method_chain anymore.

A Lesson Learned

Throughout this book I showed you how convenient, elegant, and cool metaprogramming can be. The story of alias_method_chain, however, is a cautionary tale: metaprogramming code can sometimes get complicated, and it can even cause you to overlook more traditional, simpler techniques. In particular, sometimes you can avoid metaprogramming and use plain, old-fashioned object-oriented programming instead.

The lesson I personally learned from this story is: resist the temptation to be too clever in your code. Ask yourself whether there is a simpler way to reach your goal than metaprogramming. If the answer is no, then go forth and metaprogram the heck out of your problem. In many cases, however, you’ll find that a more straightforward OOP approach does the job just as well.

In this chapter, I showed you that metaprogramming can be overused and sometimes replaced with simpler techniques. To be fair, however, metaprogramming is still one of the tastier ingredients in the Rails pie. In the next chapter, I’ll show you how one of Rails’ defining features owes its very existence to a clever mix of metaprogramming tricks.