Книга: Metaprogramming Ruby 2
Назад: Dynamic Methods
Дальше: Quiz: Bug Hunt

method_missing

Where you listen to spooky stories about Ghost Methods and dynamic proxies and you try a second way to remove duplicated code.

With Ruby, there’s no compiler to enforce method calls. This means you can call a method that doesn’t exist. For example:

 
class​ Lawyer; ​end
 
nick = Lawyer.new
 
nick.talk_simple
<= 
NoMethodError: undefined method `talk_simple' for #<Lawyer:0x007f801aa81938>

Do you remember how method lookup works? When you call talk_simple, Ruby goes into nick’s class and browses its instance methods. If it can’t find talk_simple there, it searches up the ancestors chain into Object and eventually into BasicObject.

Because Ruby can’t find talk_simple anywhere, it admits defeat by calling a method named method_missing on nick, the original receiver. Ruby knows that method_missing is there, because it’s a private instance method of BasicObject that every object inherits.

You can experiment by calling method_missing yourself. It’s a private method, but you can get to it through send:

 
nick.send :method_missing, :my_method
<= 
NoMethodError: undefined method `my_method' for #<Lawyer:0x007f801b0f4978>

You have just done exactly what Ruby does. You told the object, “I tried to call a method named my_method on you, and you did not understand.” BasicObject#method_missing responded by raising a NoMethodError. In fact, this is what method_missing does for a living. It’s like an object’s dead-letter office, the place where unknown messages eventually end up (and the place where NoMethodErrors come from).

Overriding method_missing

Most likely, you will never need to call method_missing yourself. Instead, you can override it to intercept unknown messages. Each message landing on method_missing’s desk includes the name of the method that was called, plus any arguments and blocks associated with the call.

 
class​ Lawyer
 
def​ method_missing(method, *args)
 
puts ​"You called: ​#{method}​(​#{args.join(​', '​)}​)"
 
puts ​"(You also passed it a block)"​ ​if​ block_given?
 
end
 
end
 
 
bob = Lawyer.new
 
bob.talk_simple(​'a'​, ​'b'​) ​do
 
# a block
 
end
<= 
You called: talk_simple(a, b)
 
(You also passed it a block)

Overriding method_missing allows you to call methods that don’t really exist. Let’s take a closer look at these weird creatures.

Ghost Methods

When you need to define many similar methods, you can spare yourself the definitions and just respond to calls through method_missing. This is like saying to the object, “If they ask you something and you don’t understand, do this.” From the caller’s side, a message that’s processed by method_missing looks like a regular call—but on the receiver’s side, it has no corresponding method. This trick is called a Spell: . Let’s look at some Ghost Method examples.

The Hashie Example

The Hashie gem contains a little bit of magic called Hashie::Mash. A Mash is a more powerful version of Ruby’s standard OpenStruct class: a hash-like object whose attributes work like Ruby variables. If you want a new attribute, just assign a value to the attribute, and it will spring into existence:

 
require ​'hashie'
 
 
icecream = Hashie::Mash.new
 
icecream.flavor = ​"strawberry"
 
icecream.flavor ​# => "strawberry"

This works because Hashie::Mash is a subclass of Ruby’s Hash, and its attributes are actually Ghost Methods, as a quick look at Hashie::Mash.method_missing will confirm:

 
module​ Hashie
 
class​ Mash < Hashie::Hash
 
def​ method_missing(method_name, *args, &blk)
 
return​ self.[](method_name, &blk) ​if​ key?(method_name)
 
match = method_name.to_s.match(/(.*?)([?=!]?)$/)
 
case​ match[2]
 
when​ ​"="
 
self[match[1]] = args.first
 
# ...
 
else
 
default(method_name, *args, &blk)
 
end
 
end
 
 
# ...
 
end
 
end

If the name of the called method is the name of a key in the hash (such as flavor), then Hashie::Mash#method_missing simply calls the [] method to return the corresponding value. If the name ends with a "=", then method_missing chops off the "=" at the end to get the attribute name and then stores its value. If the name of the called method doesn’t match any of these cases, then method_missing just returns a default value. (Hashie::Mash also supports a few other special cases, such as methods ending in "?", that were scrapped from the code above.)

Dynamic Proxies

Ghost Methods () are usually icing on the cake, but some objects actually rely almost exclusively on them. These objects are often wrappers for something else—maybe another object, a web service, or code written in a different language. They collect method calls through method_missing and forward them to the wrapped object. Let’s look at a complex real-life example.

The Ghee Example

You probably know GitHub, the wildly popular social coding service. A number of libraries give you easy access to GitHub’s HTTP APIs, including a Ruby gem called Ghee. Here is how you use Ghee to access a user’s “gist”—a snippet of code that can be published on GitHub:

 
require ​"ghee"
 
 
gh = Ghee.basic_auth(​"usr"​, ​"pwd"​) ​# Your GitHub username and password
 
all_gists = gh.users(​"nusco"​).gists
 
a_gist = all_gists[20]
 
 
a_gist.url ​# => "https://api.github.com/gists/535077"
 
a_gist.description ​# => "Spell: Dynamic Proxy"
 
 
a_gist.star

The code above connects to GitHub, looks up a specific user ("nusco"), and accesses that user’s list of gists. Then it selects one specific gist and reads that gist’s url and description. Finally, it “stars” the gist, to be notified of any future changes.

The GitHub APIs expose tens of types of objects besides gists, and Ghee has to support all of those objects. However, Ghee’s source code is surprisingly concise, thanks to a smart use of Ghost Methods (). Most of the magic happens in the Ghee::ResourceProxy class:

 
class​ Ghee
 
class​ ResourceProxy
 
# ...
 
 
def​ method_missing(message, *args, &block)
 
subject.send(message, *args, &block)
 
end
 
 
def​ subject
 
@subject ||= connection.get(path_prefix){|req| req.params.merge!params }.body
 
end
 
end
 
end

Before you understand this class, you need to see how Ghee uses it. For each type of GitHub object, such as gists or users, Ghee defines one subclass of Ghee::ResourceProxy. Here is the class for gists (the class for users is quite similar):

 
class​ Ghee
 
module​ API
 
module​ Gists
 
class​ Proxy < ::Ghee::ResourceProxy
 
def​ star
 
connection.put(​"​#{path_prefix}​/star"​).status == 204
 
end
 
 
# ...
 
 
end
 
end
 
end

When you call a method that changes the state of an object, such as Ghee::API::Gists#star, Ghee places an HTTP call to the corresponding GitHub URL. However, when you call a method that just reads from an attribute, such as url or description, that call ends into Ghee::ResourceProxy#method_missing. In turn, method_missing forwards the call to the object returned by Ghee::ResourceProxy#subject. What kind of object is that?

If you dig into the implementation of ResourceProxy#subject, you’ll find that this method also makes an HTTP call to the GitHub API. The specific call depends on which subclass of Ghee::ResourceProxy we’re using. For example, Ghee::API::Gists::Proxy calls . ResourceProxy#subject receives the GitHub object in JSON format—in our example, all the gists of user nusco—and converts it to a hash-like object.

Dig a little deeper, and you’ll find that this hash-like object is actually a Hashie::Mash, the magic hash class that we talked about in . This means that a method call such as my_gist.url is forwarded to Ghee::ResourceProxy#method_missing, and from there to Hashie::Mash#method_missing, which finally returns the value of the url attribute. Yes, that’s two calls to method_missing in a row.

Ghee’s design is elegant, but it uses so much metaprogramming that it might confuse you at first. Let’s wrap it up in just two points:

Thanks to this two-level design, Ghee manages to keep its code very compact. It doesn’t need to define methods that just read data, because those methods are Ghost Methods. Instead, it can just define the methods that need specific code, like star.

This dynamic approach also has another advantage: Ghee can adapt automatically to some changes in the GitHub APIs. For example, if GitHub added a new field to gists (say, lines_count), Ghee would support calls to Ghee::API::Gists#lines_count without any changes to its source code, because lines_count is just a Ghost Method—actually a chain of two Ghost Methods.

An object such as Ghee::ResourceProxy, which catches Ghost Methods and forwards them to another object, is called a Spell: .

Refactoring the Computer Class (Again)

“Okay, you now know about method_missing,” Bill says. “Let’s go back to the Computer class and remove the duplication.”

Once again, here’s the original Computer class:

 
class​ Computer
 
def​ initialize(computer_id, data_source)
 
@id = computer_id
 
@data_source = data_source
 
end
 
 
def​ mouse
 
info = @data_source.get_mouse_info(@id)
 
price = @data_source.get_mouse_price(@id)
 
result = ​"Mouse: ​#{info}​ ($​#{price}​)"
 
return​ ​"* ​#{result}​"​ ​if​ price >= 100
 
result
 
end
 
 
def​ cpu
 
info = @data_source.get_cpu_info(@id)
 
price = @data_source.get_cpu_price(@id)
 
result = ​"Cpu: ​#{info}​ ($​#{price}​)"
 
return​ ​"* ​#{result}​"​ ​if​ price >= 100
 
result
 
end
 
 
def​ keyboard
 
info = @data_source.get_keyboard_info(@id)
 
price = @data_source.get_keyboard_price(@id)
 
result = ​"Keyboard: ​#{info}​ ($​#{price}​)"
 
return​ ​"* ​#{result}​"​ ​if​ price >= 100
 
result
 
end
 
 
# ...
 
end

Computer is just a wrapper that collects calls, tweaks them a bit, and routes them to a data source. To remove all those duplicated methods, you can turn Computer into a Dynamic Proxy. It only takes an override of method_missing to remove all the duplication from the Computer class.

 
class​ Computer
 
def​ initialize(computer_id, data_source)
 
@id = computer_id
 
@data_source = data_source
 
end
 
*
def​ method_missing(name)
*
super​ ​if​ !@data_source.respond_to?(​"get_​#{name}​_info"​)
*
info = @data_source.send(​"get_​#{name}​_info"​, @id)
*
price = @data_source.send(​"get_​#{name}​_price"​, @id)
*
result = ​"​#{name.capitalize}​: ​#{info}​ ($​#{price}​)"
*
return​ ​"* ​#{result}​"​ ​if​ price >= 100
*
result
*
end
 
end

What happens when you call a method such as Computer#mouse? The call gets routed to method_missing, which checks whether the wrapped data source has a get_mouse_info method. If it doesn’t have one, the call falls back to BasicObject#method_missing, which throws a NoMethodError. If the data source knows about the component, the original call is converted into two calls to DS#get_mouse_info and DS#get_mouse_price. The values returned from these calls are used to build the final result. You try the new class in irb:

 
my_computer = Computer.new(42, DS.new)
 
my_computer.cpu ​# => * Cpu: 2.9 Ghz quad-core ($120)

It worked. Bill, however, is concerned about one last detail.

respond_to_missing?

If you specifically ask a Computer whether it responds to a Ghost Method, it will flat-out lie:

 
cmp = Computer.new(0, DS.new)
 
cmp.respond_to?(:mouse) ​# => false

This behavior can be problematic, because respond_to? is a commonly used method. (If you need convincing, just note that the Computer itself is calling respond_to? on the data source.) Fortunately, Ruby provides a clean mechanism to make respond_to? aware of Ghost Methods.

respond_to? calls a method named respond_to_missing? that is supposed to return true if a method is a Ghost Method. (In your mind, you could rename respond_to_missing? to something like ghost_method?.) To prevent respond_to? from lying, override respond_to_missing? every time you override method_missing:

 
class​ Computer
 
# ...
 
*
def​ respond_to_missing?(method, include_private = false)
*
@data_source.respond_to?(​"get_​#{method}​_info"​) || ​super
*
end
 
end

The code in this respond_to_missing? is similar to the first line of method_missing: it finds out whether a method is a Ghost Method. If it is, it returns true. If it isn’t, it calls super. In this case, super is the default Object#respond_to_missing?, which always returns false.

Now respond_to? will learn about your Ghost Methods from respond_to_missing? and return the right result:

 
cmp.respond_to?(:mouse) ​# => true

Back in the day, Ruby coders used to override respond_to? directly. Now that respond_to_missing? is available, overriding respond_to? is considered somewhat dirty. Instead, the rule is now this: remember to override respond_to_missing? every time you override method_missing.

If you like BasicObject#method_missing, you should also take a look at Module#const_missing. Let’s check it out.

const_missing

Remember our discussion of Rake in ? In that section we said that at one point in its history, Rake renamed classes like Task to names that are less likely to clash, such as Rake::Task. After renaming the classes, Rake went through an upgrade path: for a few versions, you could use either the new class names or the old, non-Namespaced names. Rake allowed you to do that by Monkepatching () the Module#const_missing method:

 
class​ Module
 
def​ const_missing(const_name)
 
case​ const_name
 
when​ :Task
 
Rake.application.const_warning(const_name)
 
Rake::Task
 
when​ :FileTask
 
Rake.application.const_warning(const_name)
 
Rake::FileTask
 
when​ :FileCreationTask
 
# ...
 
end
 
end
 
end

When you reference a constant that doesn’t exist, Ruby passes the name of the constant to const_missing as a symbol. Class names are just constants, so a reference to an unknown Rake class such as Task was routed to Module#const_missing. In turn, const_missing warned you that you were using an obsolete class name:

 
require ​'rake'
 
task_class = Task
<= 
WARNING: Deprecated reference to top-level constant 'Task' found [...]
 
Use --classic-namespace on rake command
 
or 'require "rake/classic_namespace"' in Rakefile

After the warning, you automatically got the new, Namespaced class name in place of the old one:

 
task_class ​# => Rake::Task

Enough talking about magic methods. Let’s recap what you and Bill did today.

Refactoring Wrap-Up

Today you solved the same problem in two different ways. The first version of Computer introspects DS to get a list of methods to wrap and uses Dynamic Methods () and Dynamic Dispatches (), which delegate to the legacy system. The second version of Computer does the same with Ghost Methods (). Having to pick one of the two versions, you and Bill randomly select the method_missing-based one, send it to the folks in purchasing, and head out for a well-deserved lunch break…and an unexpected quiz.

Назад: Dynamic Methods
Дальше: Quiz: Bug Hunt