Where you learn how to call and define methods dynamically, and you remove the duplicated code.
“When I was a young developer learning C++,” Bill says, “my mentors told me that when you call a method, you’re actually sending a message to an object. It took me a while to get used to that concept. If I’d been using Ruby back then, that notion of sending messages would have come more naturally to me.”
When you call a method, you usually do so using the standard dot notation:
| class MyClass |
| def my_method(my_arg) |
| my_arg * 2 |
| end |
| end |
| |
| obj = MyClass.new |
| obj.my_method(3) # => 6 |
You also have an alternative: call MyClass#my_method using Object#send in place of the dot notation:
| obj.send(:my_method, 3) # => 6 |
The previous code still calls my_method, but it does so through send. The first argument to send is the message that you’re sending to the object—that is, a symbol or a string representing the name of a method. (See .) Any remaining arguments (and the block, if one exists) are simply passed on to the method.
Why would you use send instead of the plain old dot notation? Because with send, the name of the method that you want to call becomes just a regular argument. You can wait literally until the very last moment to decide which method to call, while the code is running. This technique is called Spell: , and you’ll find it wildly useful. To help reveal its magic, let’s look at a couple of real-life examples.
One example of Dynamic Dispatch comes from Pry. Pry is a popular alternative to irb, Ruby’s command-line interpreter. A Pry object stores the interpreter’s configuration into its own attributes, such as memory_size and quiet:
| require "pry" |
| |
| pry = Pry.new |
| pry.memory_size = 101 |
| pry.memory_size # => 101 |
| pry.quiet = true |
For each instance method like Pry#memory_size, there is a corresponding class method (Pry.memory_size) that returns the default value of the attribute:
| Pry.memory_size # => 100 |
Let’s look a little deeper inside the Pry source code. To configure a Pry instance, you can call a method named Pry#refresh. This method takes a hash that maps attribute names to their new values:
| pry.refresh(:memory_size => 99, :quiet => false) |
| pry.memory_size # => 99 |
| pry.quiet # => false |
Pry#refresh has a lot of work to do: it needs to go through each attribute (such as self.memory_size); initialize the attribute with its default value (such as Pry.memory_size); and finally check whether the hash argument contains a new value for the same attribute, and if it does, set the new value. Pry#refresh could do all of those steps with code like this:
| def refresh(options={}) |
| defaults[:memory_size] = Pry.memory_size |
| self.memory_size = options[:memory_size] if options[:memory_size] |
| |
| defaults[:quiet] = Pry.quiet |
| self.quiet = options[:quiet] if options[:quiet] |
| # same for all the other attributes... |
| end |
Those two lines of code would have to be repeated for each and every attribute. That’s a lot of duplicated code. Pry#refresh manages to avoid that duplication, and instead uses Dynamic Dispatch () to set all the attributes with just a few lines of code:
| def refresh(options={}) |
| defaults = {} |
| attributes = [ :input, :output, :commands, :print, :quiet, |
| :exception_handler, :hooks, :custom_completions, |
| :prompt, :memory_size, :extra_sticky_locals ] |
| |
| attributes.each do |attribute| |
| defaults[attribute] = Pry.send attribute |
| end |
| # ... |
| defaults.merge!(options).each do |key, value| |
| send("#{key}=", value) if respond_to?("#{key}=") |
| end |
| |
| true |
| end |
The code above uses send to read the default attribute values into a hash, merges this hash with the options hash, and finally uses send again to call attribute accessors such as memory_size=. The Kernel#respond_to? method returns true if methods such as Pry#memory_size= actually exist, so that any key in options that doesn’t match an existing attribute will be ignored. Neat, huh?
Remember what Spiderman’s uncle used to say? “With great power comes great responsibility.” The Object#send method is very powerful—perhaps too powerful. In particular, you can call any method with send, including private methods.
If that kind of breaching of encapsulation makes you uneasy, you can use public_send instead. It’s like send, but it makes a point of respecting the receiver’s privacy. Be prepared, however, for the fact that Ruby code in the wild rarely bothers with this concern. If anything, a lot of Ruby programmers use send exactly because it allows calling private methods, not in spite of that.
Now you know about send and Dynamic Dispatch—but there is more to Dynamic Methods than that. You’re not limited to calling methods dynamically. You can also define methods dynamically. It’s time to see how.
You can define a method on the spot with Module#define_method. You just need to provide a method name and a block, which becomes the method body:
| class MyClass |
| define_method :my_method do |my_arg| |
| my_arg * 3 |
| end |
| end |
| |
| obj = MyClass.new |
| obj.my_method(2) # => 6 |
| |
| require_relative '../test/assertions' |
| assert_equals 6, obj.my_method(2) |
define_method is executed within MyClass, so my_method is defined as an instance method of MyClass. This technique of defining a method at runtime is called a Spell: .
There is one important reason to use define_method over the more familiar def keyword: define_method allows you to decide the name of the defined method at runtime. To see an example of this technique, look back at your original refactoring problem.
Recall the code that pulled you and Bill into this dynamic discussion:
| 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 |
In the previous pages you learned how to use Module#define_method in place of the def keyword to define a method, and how to use send in place of the dot notation to call a method. Now you can use these spells to refactor the Computer class. It’s time to remove some duplication.
You and Bill start by extracting the duplicated code into its own message-sending method:
| class Computer |
| def initialize(computer_id, data_source) |
| @id = computer_id |
| @data_source = data_source |
| end |
| |
* | def mouse |
* | component :mouse |
* | end |
* | |
* | def cpu |
* | component :cpu |
* | end |
* | |
* | def keyboard |
* | component :keyboard |
* | end |
* | |
* | def component(name) |
* | 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 |
A call to mouse is delegated to component, which in turn calls DS#get_mouse_info and DS#get_mouse_price. The call also writes the capitalized name of the component in the resulting string. You open an irb session and smoke-test the new Computer:
| my_computer = Computer.new(42, DS.new) |
| my_computer.cpu # => * Cpu: 2.16 Ghz ($220) |
This new version of Computer is a step forward because it contains far fewer duplicated lines—but you still have to write dozens of similar methods. To avoid writing all those methods, you can turn to define_method.
You and Bill refactor Computer to use Dynamic Methods (), as shown in the following code.
| class Computer |
| def initialize(computer_id, data_source) |
| @id = computer_id |
| @data_source = data_source |
| 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 |
* | |
* | define_component :mouse |
* | define_component :cpu |
* | define_component :keyboard |
| end |
Note that the three calls to define_component are executed inside the definition of Computer, where Computer is the implicit self. Because you’re calling define_component on Computer, you have to make it a class method.
You quickly test the slimmed-down Computer class in irb and discover that it still works. It’s time to move on to the next step.
The latest Computer contains minimal duplication, but you can push it even further and remove the duplication altogether. How? By getting rid of all those calls to define_component. You can do that by introspecting the data_source argument and extracting the names of all components:
| 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 |
| # ... |
| end |
| end |
| end |
The new line in initialize is where the magic happens. To understand it, you need to know a couple of things.
First, if you pass a block to Array#grep, the block is evaluated for each element that matches the regular expression. Second, the string matching the parenthesized part of the regular expression is stored in the global variable $1. So, if data_source has methods named get_cpu_info and get_mouse_info, this code ultimately calls Computer.define_component twice, with the strings "cpu" and "mouse". Note that define_method works equally well with a string or a symbol.
The duplicated code is finally gone for good. As a bonus, you don’t even have to write or maintain the list of components. If someone adds a new component to DS, the Computer class will support it automatically. Isn’t that wonderful?
Your refactoring was a resounding success, but Bill is not willing to stop here. “We said that we were going to try two different solutions to this problem, remember? We’ve only found one, involving Dynamic Dispatch () and Dynamic Methods (). It did serve us well—but to be fair, we need to give the other solution a chance.”
For this second solution, you need to know about some strange methods that are not really methods and a very special method named method_missing.