Let’s review today’s work. You and Bill started with a Computer class that contained lots of duplication. (The original class is in .) You removed the duplication in two different ways.
Your first attempt relied on Dynamic Methods () and Dynamic Dispatch ():
| class Computer |
| def initialize(computer_id, data_source) |
| @id = computer_id |
| @data_source = data_source |
| data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 } |
| end |
| |
| def self.define_component(name) |
| define_method(name) do |
| 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 |
| end |
Your second attempt centered around Ghost Methods () (to be more precise, it used a Dynamic Proxy () that is also a Blank Slate ()):
| class Computer < BasicObject |
| def initialize(computer_id, data_source) |
| @id = computer_id |
| @data_source = data_source |
| end |
| |
| def method_missing(name, *args) |
| 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 |
Neither solution would be practical without Ruby’s dynamic capabilities. If you come from a static language, you’re probably accustomed to spotting and removing duplication inside your methods. In Ruby, you might want to look for duplication among methods as well. Then you can remove that duplication with some of the spells you’ve learned today.
You and Bill can consider the two solutions. It’s time to make a choice. Which one do you like best?
As you experienced yourself, Ghost Methods () can be dangerous. You can avoid most of their problems by following a few basic recommendations (always call super, always redefine respond_to_missing?)—but even then, Ghost Methods can sometimes cause puzzling bugs.
The problems with Ghost Methods boil down to the fact that they are not really methods; instead, they’re just a way to intercept method calls. Because of this, they behave differently from actual methods. For example, they don’t appear in the list of names returned by Object#methods. In contrast, Dynamic Methods are just regular methods that happened to be defined with define_method instead of def, and they behave the same as any other method.
There are times when Ghost Methods are your only viable option. This usually happens when you have a large number of method calls, or when you don’t know what method calls you might need at runtime. For an example, look back at the Builder library in . Builder couldn’t define a Dynamic Method for each of the potentially infinite XML tags that you might want to generate, so it uses method_missing to intercept method calls instead.
All things considered, the choice between Dynamic and Ghost Methods depends on your experience and coding style, but you can follow a simple rule of thumb when in doubt: use Dynamic Methods if you can and Ghost Methods if you have to.
You and Bill decide to follow this rule, and you commit the define_method-based version of Computer to the project repository. Tomorrow is sure to be another day of coding challenges, so it’s time to head home and rest up.
A presentation on the perils of method_missing is at . |