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).
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.
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 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.)
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.
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:
Ghee stores GitHub objects as dynamic hashes. You can access the attributes of these hashes by calling their Ghost Methods (), such as url and description.
Ghee also wraps these hashes inside proxy objects that enrich them with additional methods. A proxy does two things. First, it implements methods that require specific code, such as star. Second, it forwards methods that just read data, such as url, to the wrapped hash.
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: .
“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.
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.
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.
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.