Where you learn how to wrap a method inside another method—three different ways.
As the day draws to a close, you and Bill find yourselves stuck. Many methods in Bookworm rely on an open source library that retrieves a book’s reviews from Amazon’s website. The following code shows one example:
| def deserves_a_look?(book) |
| amazon = Amazon.new |
| amazon.reviews_of(book).size > 20 |
| end |
This code works in most cases, but it doesn’t manage exceptions. If a remote call to Amazon fails, Bookworm itself should log this problem and proceed. You could easily add exception management to each line in Bookworm that calls deserves_a_look?—but there are tens of such lines, and you don’t want to change all of them.
To sum up the problem: you have a method that you don’t want to modify directly because it’s in a library. You want to wrap additional functionality around this method so that all clients get the additional functionality automatically. You can do this in a few ways, but to get to the first of them you need to know about aliases.
You can give an alternate name to a Ruby method by using Module#alias_method:
| class MyClass |
| def my_method; 'my_method()'; end |
| alias_method :m, :my_method |
| end |
| |
| obj = MyClass.new |
| obj.my_method # => "my_method()" |
| obj.m # => "my_method()" |
In alias_method, the new name for the method comes first, and the original name comes second. You can provide the names either as symbols or as strings.
(Ruby also has an alias keyword, which is an alternative to Module#alias_method. It can be useful if you want to alias a method at the top level, where Module#alias_method is not available.)
Continuing with the previous example:
| class MyClass |
| alias_method :m2, :m |
| end |
| |
| obj.m2 # => "my_method()" |
Aliases are common everywhere in Ruby, including the core libraries. For example, String#size is an alias of String#length, and the Integer class has a method with no fewer than five different names. (Can you spot it?)
What happens if you alias a method and then redefine it? You can try this with a simple program:
| class String |
| alias_method :real_length, :length |
| |
| def length |
| real_length > 5 ? 'long' : 'short' |
| end |
| end |
| |
| "War and Peace".length # => "long" |
| "War and Peace".real_length # => 13 |
The previous code redefines String#length, but the alias still refers to the original method. This gives you insight into how method redefinition works. When you redefine a method, you don’t really change the method. Instead, you define a new method and attach an existing name to that new method. You can still call the old version of the method as long as you have another name that’s still attached to it.
This idea of aliasing a method and then redefining it is the basis of an interesting trick—one that deserves its own example.
Thor is a Ruby gem for building command-line utilities. Some versions of Thor include a program named rake2thor that converts Rake build files to Thor scripts. As part of doing that, rake2thor must load a Rakefile and store away the names of all the files that are in turn required from that Rakefile. Here is the code where the magic happens:
| input = ARGV[0] || 'Rakefile' |
| $requires = [] |
| |
| module Kernel |
| def require_with_record(file) |
| $requires << file if caller[1] =~ /rake2thor:/ |
| require_without_record file |
| end |
| alias_method :require_without_record, :require |
| alias_method :require, :require_with_record |
| end |
| |
| load input |
The code above prepares a global array to store the names of the required files; then it opens the Kernel module and plays a few tricks with method aliases; and finally, it loads the Rakefile. Focus on the middle part—the code dealing with Kernel. To understand what is going on there, look at this slightly simplified version of the original code:
| module Kernel |
| alias_method :require_without_record, :require |
| |
| def require(file) |
| $requires << file if caller[1] =~ /rake2thor:/ |
| require_without_record file |
| end |
| end |
The Open Class () above does three things. First, it aliases the standard Kernel#require method to another name (require_without_record). Second, it Monkeypatches () require to store the names of files that are required by the Rakefile. (It does that by getting the stack of callers with the Kernel#callers method. If the second caller in the stack is rake2thor itself, this means that the Rakefile must be the first caller in the stack—the one that actually called require.) Finally, the new require falls back to the original require, now called require_without_record.
Compared to this simplified version, the original rake2thor code goes one step further: it also creates an alias for the new require called require_with_record. While this latest alias makes the methods more explicit, the important result is pretty much the same in both versions of the code: Kernel#require has changed, and the new require is “wrapped around” the old require. That’s why this trick is called an Spell: .
You can write an Around Alias in three simple steps:
You alias a method.
You redefine it.
You call the old method from the new method.
One downside of Around Aliases is that they pollute your classes with one additional method name. You can fix this small problem somehow by making the old version of the method private after you alias it. (In Ruby it’s the method’s name, not the method itself, that is either public or private.)
Another potential problem of Around Aliases has to do with loading. You should never load an Around Alias twice, unless you want to end up with an exception when you call the method. Can you see why?
The main issue with Around Aliases, however, is that they are a form of Monkeypatching. Like all Monkeypatches, they can break existing code that wasn’t expecting the method to change. For this reason, Ruby 2.0 introduced not one, but two additional ways to wrap additional functionality around an existing method.
In , you learned that a Refinement () works like a patch of code that has been slapped directly over a class. However, Refinements have one additional feature that enables you to use them in place of Around Aliases (): if you call super from a refined method, you will call the original, unrefined method. Here comes an example:
| module StringRefinement |
| refine String do |
| def length |
| super > 5 ? 'long' : 'short' |
| end |
| end |
| end |
| |
| using StringRefinement |
| |
| "War and Peace".length # => "long" |
The code above refines the String class to wrap additional functionality around its length method. Like other Refinements, this Spell: applies only until the end of the file (or, in Ruby 2.1, the module definition). This makes it generally safer than the equivalent Around Alias, which applies everywhere.
Finally, you have a third way of wrapping a method: you can use Module#prepend, which you might remember from . Module#prepend works a bit like include, but it inserts the module below the includer in the chain of ancestors, rather than above it. This means that a method in a prepended module can override a method in the includer and call the non-overridden version with super:
| module ExplicitString |
| def length |
| super > 5 ? 'long' : 'short' |
| end |
| end |
| |
| String.class_eval do |
| prepend ExplicitString |
| end |
| |
| "War and Peace".length # => "long" |
You can call this a Spell: . It’s not local like a Refinement Wrapper, but it’s generally considered cleaner and more explicit than both a Refinement Wrapper and an Around Alias.
Now you know more than enough to get back to the Bookworm source code.
Remember where this discussion of method wrappers originated? You and Bill wanted to wrap logging and exception handling around the Amazon#reviews_of method. Now you can finally do that with an Around Alias (), a Refinement Wrapper (), or a Prepended Wrapper (). The third option looks cleaner, as it doesn’t dabble in Monkeypatching or weird Refinement rules:
| module AmazonWrapper |
| def reviews_of(book) |
| start = Time.now |
| result = super |
| time_taken = Time.now - start |
| puts "reviews_of() took more than #{time_taken} seconds" if time_taken > 2 |
| result |
| rescue |
| puts "reviews_of() failed" |
| [] |
| end |
| end |
| |
| Amazon.class_eval do |
| prepend AmazonWrapper |
| end |
As you admire this smart piece of code, Bill hits you with an unexpected quiz.