Книга: Metaprogramming Ruby 2
Назад: Quiz: Module Trouble
Дальше: Quiz: Broken Math

Method Wrappers

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.

Around 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.

The Thor 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:

  1. You alias a method.

  2. You redefine it.

  3. 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.

More Method Wrappers

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.

Solving the Amazon Problem

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.

Назад: Quiz: Module Trouble
Дальше: Quiz: Broken Math