A memo of what you learned by reading Chapter 3 Methods of Metaprogramming Ruby.
The content is to offer a solution to the problem of duplicate code in method definitions.
――There are two main types of solutions.
define_method
)method_missing
)
--Use dynamic methods whenever possible, and ghost methods when it can't be helped.We will proceed based on the concrete example of "creating a system that detects computer parts that cost more than $ 99."
How to improve such a method with a lot of code duplication ...
#Computer class with lots of duplication
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
Use ʻObject # send (ʻobj.send (: my_method, arg)
) instead of the usual dot notation (ʻobj.my_method (arg) ) to call the method. By using
send`, the method name you want to call becomes an argument, and you can dynamically specify the method name.
Such a technique is called ** dynamic dispatch **.
#Refactoring with dynamic dispatch
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_souce.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
You can use Module # define_method
to dynamically define a method.
You need to pass the method name and block, and the block becomes the body of the method.
Here, we want to call define_method
in the Computer class definition, so we need to make it a class method.
# define_Further refactoring using method
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})"
retrun "* #{result}" if price >= 100
result
end
end
define_component :mouse
define_component :cpu
define_component :keyboard
end
Then you can use it like this
obj = Computer.new(42, data_source)
obj.mouse # => "Wireless Touch"
obj.price # => 60
To further eliminate duplication, install data_source
(*) and expand it to the name of the component.
# data_Instrope source!
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
# get_xxx_Pass the block to a list of methods called info and
#A string that matches the regular expression(mouse,cpu etc.)Define a method with the name of
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})"
retrun "* #{result}" if price >= 100
result
end
end
end
Now, even if a component is added on the data_source
side, it can be supported without tampering with the Computer class.
(*) Instrumentation ... Asking an object a language element (variable, class, method, etc.)
"hoge".class #Ask the class
=> String
"hoge".methods.grep(/to_(.*)/) # "to_"Listen to the methods that start with
=> [:to_c, :to_str, :to_sym, :to_s, :to_i, :to_f, :to_r, :to_json_raw, :to_json_raw_object, :to_json, :to_enum]
method_missing
When you call a method that does not exist, BasicObject # method_missing
is called.
This is a common feeling.
class Lawyer; end
nick = Lawyer.new
nick.talk
=> NoMethodError: undefined method `talk' for #<Lawyer:0x00007f921c0f2958>
By overriding this method_missing
, you can call a method that doesn't actually exist.
Such a method that is processed by method_missing
but has no corresponding method on the receiver side is called ** ghost method **.
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name)
# @data_If there is no corresponding method in source, method of parent class_Call missing
super if !@data_source.respond_to?("get_#{name}_info")
#If there is a method, do the following
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
def respond_to_missing?(method, include_private = false)
@data_source.respond_to?("get_#{method}_info") || super
end
end
method_missing
bug is hard to crushExample)
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}" #An infinite loop occurs here. I wonder why?
end
end
Another trap for method_missing
In the previous Computer
class, only the display
method does not work properly.
my_computer = Computer.new(42, DS.new)
my_computer.display # => nil
why.
=> Because the display
method is already defined in the inherited ʻObject` class.
Object.instance_methods.grep /^d/
=> [:define_singleton_method, :display, :dup]
I think I'm calling Computer # display
, but I can't get to method_missing
because ʻObject # display` is found.
To solve this, unnecessary methods need to be deleted. A class with minimal methods is called a ** blank slate **.
As a method of realizing a blank slate, a method of inheriting the BasicObject
class and a method of deleting unnecessary methods are introduced.
As you can see, the ghost method carries the risk of including bugs that are useful but hard to find. So, the following conclusion.
** "Use dynamic methods whenever possible, and ghost methods when it can't be helped." **
I'm going home and resting today.
Recommended Posts