Where you discover that bugs in a method_missing can be difficult to squash.
Over lunch, Bill has a quiz for you. “My previous team followed a cruel office ritual,” he says. “Every morning, each team member picked a random number. Whoever got the smallest number had to take a trip to the nearby Starbucks and buy coffee for the whole team.”
Bill explains that the team even wrote a class that was supposed to provide a random number (and some Wheel of Fortune--style suspense) when you called the name of a team member. Here’s the class:
| class Roulette |
| def method_missing(name, *args) |
| person = name.to_s.capitalize |
| 3.times do |
| number = rand(10) + 1 |
| puts "#{number}..." |
| end |
| "#{person} got a #{number}" |
| end |
| end |
You can use the Roulette like this:
| number_of = Roulette.new |
| puts number_of.bob |
| puts number_of.frank |
And here’s what the result is supposed to look like:
<= | 5... |
| 6... |
| 10... |
| Bob got a 3 |
| 7... |
| 4... |
| 3... |
| Frank got a 10 |
“This code was clearly overdesigned,” Bill admits. “We could have just defined a regular method that took the person’s name as a string—but we’d just discovered method_missing, so we used Ghost Methods () instead. That wasn’t a good idea; the code didn’t work as expected.”
Can you spot the problem with the Roulette class? If you can’t, try running it on your computer. Now can you explain what is happening?
The Roulette contains a bug that causes an infinite loop. It prints a long list of numbers and finally crashes.
<= | 2... |
| 7... |
| 1... |
| 5... |
| (...more numbers here...) |
| roulette_failure.rb:7:in `method_missing': stack level too deep (SystemStackError) |
This bug is nasty and difficult to spot. The variable number is defined within a block (the block that gets passed to times) and falls out of scope by the last line of method_missing. When Ruby executes that line, it can’t know that the number there is supposed to be a variable. As a default, it assumes that number must be a parentheses-less method call on self.
In normal circumstances, you would get an explicit NoMethodError that makes the problem obvious. But in this case you have a method_missing, and that’s where the call to number ends. The same chain of events happens again—and again and again—until the call stack overflows.
This is a common problem with Ghost Methods: because unknown calls become calls to method_missing, your object might accept a call that’s just plain wrong. Finding a bug like this one in a large program can be pretty painful.
To avoid this kind of trouble, take care not to introduce too many Ghost Methods. For example, Roulette might be better off if it simply accepted the names of people on Frank’s team. Also, remember to fall back on BasicObject#method_missing when you get a call you don’t know how to handle. Here’s a better Roulette that still uses method_missing:
| class Roulette |
| def method_missing(name, *args) |
| person = name.to_s.capitalize |
* | super unless %w[Bob Frank Bill].include? person |
* | number = 0 |
| 3.times do |
| number = rand(10) + 1 |
| puts "#{number}..." |
| end |
| "#{person} got a #{number}" |
| end |
| end |
You can also develop this code in bite-sized steps. Start by writing regular methods; then, when you’re confident that your code is working, refactor the methods to a method_missing. This way, you won’t inadvertently hide a bug behind a Ghost Method.