Книга: Metaprogramming Ruby 2
Назад: A Duplication Problem
Дальше: method_missing

Dynamic Methods

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.”

Calling Methods Dynamically

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.

The Pry Example

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?

Privacy Matters

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.

Defining Methods Dynamically

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.

Refactoring the Computer Class

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.

Step 1: Adding Dynamic Dispatches

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.

Step 2: Generating Methods Dynamically

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.

Step 3: Sprinkling the Code with Introspection

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?

Let’s Try That Again

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.

Назад: A Duplication Problem
Дальше: method_missing