[Learning Memo] Metaprogramming Ruby 2nd Edition: Chapter 3: Methods

Overview

A memo of what you learned by reading Chapter 3 Methods of Metaprogramming Ruby.

First rough summary

The content is to offer a solution to the problem of duplicate code in method definitions.

――There are two main types of solutions.

  1. Dynamic method (define_method)
  2. Ghost method (method_missing) --Use dynamic methods whenever possible, and ghost methods when it can't be helped.

example

We will proceed based on the concrete example of "creating a system that detects computer parts that cost more than $ 99."

Duplicate code problem

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

Solution using dynamic methods

Dynamic dispatch

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

Dynamic method

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

Further instrumentation

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]

Solution using the ghost method

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

The method_missing bug is hard to crush

Example)

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

Blank slate

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.

Conclusion

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

[Learning Memo] Metaprogramming Ruby 2nd Edition: Chapter 3: Methods
Ruby Learning # 15 Methods
Ruby Learning # 31 Object Methods
Effective Java 3rd Edition Chapter 8 Methods
Rails Tutorial (4th Edition) Memo Chapter 6
Rails Tutorial 6th Edition Learning Summary Chapter 10
Ruby learning 4
Rails Tutorial 6th Edition Learning Summary Chapter 7
Ruby learning 5
Rails Tutorial 6th Edition Learning Summary Chapter 4
Rails Tutorial 6th Edition Learning Summary Chapter 9
Rails Tutorial 6th Edition Learning Summary Chapter 6
Ruby learning 3
Rails Tutorial 6th Edition Learning Summary Chapter 5
Rails Tutorial 6th Edition Learning Summary Chapter 2
Ruby learning 2
Output using methods and constants Learning memo
Ruby learning 6
Rails Tutorial 6th Edition Learning Summary Chapter 3
Ruby memo
Ruby learning 1
Rails Tutorial 6th Edition Learning Summary Chapter 8
Ruby Learning # 25 Comments
Ruby Learning # 13 Arrays
Ruby Learning # 1 Introduction
Ruby Learning # 34 Modules
Ruby Learning # 14 Hashes
Ruby Learning # 33 Inheritance
Pedometer (Ruby edition)
About Ruby methods
Ruby on Rails5 Quick Learning Practice Guide 5.2 Compatible Chapter2
Ruby on Rails5 Quick Learning Practice Guide 5.2 Compatible Chapter3