メタプログラミングRuby第2版:3章:メソッド

スポンサーリンク

メタプログラミングRubyとは?

出版社である O’Reilly Japanのサイトからの引用です。

本書はRubyを使ったメタプログラミングについて解説する書籍です。メタプログラミングとは、プログラミングコードを記述するコードを記述することを意味します。前半では、メタプログラミングの背景にある基本的な考えを紹介しながら、動的ディスパッチ、ゴーストメソッド、フラットスコープといったさまざまな「魔術」を紹介します。後半では、ケーススタディとしてRailsを使ったメタプログラミングの実例を紹介します。今回の改訂では、Ruby 2とRails 4に対応し、ほぼすべての内容を刷新。Rubyを使ったメタプログラミングの魔術をマスターし、自由自在にプログラミングをしたい開発者必携の一冊です。

引用:https://www.oreilly.co.jp/books/9784873117430/

3章:メソッドを写経し終えたので内容を振り返ってまとめてみました。

Computerクラスの重複問題

コードの繰り返しが多いメソッド群をどう解決するか?

mouseメソッド、cpuメソッド、keyboardメソッドの中身は殆ど同じ。。

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

静的型付けと動的型付け

  • 全てのメソッド呼び出しに対して合致するメソッドをオブジェクトが持っているかどうかをコンパイラがチェックする
  • コードを実行する前にコンパイラがミスを指摘してくれる
  • この仕組みを使う言語を静的言語と呼ぶ
  • Rubyは実行してもエラーになる動的言語の一種

重複問題の解決策

動的言語の特性を活かして

  1. 動的メソッドを用いる方法
  2. method_missingという特別なメソッドを用いる方法

がある。

結論から言うと、可能な限り動的メソッドで対応するべき

動的メソッド

動的な呼び出し(動的ディスパッチ)

  • ドット記法ではなく、Object#sendでメソッドを呼び出す
  • 第一引数:メソッド名、第二引数:メソッドに渡す引数
  • 第一引数のメソッド名はシンボルで書くことが一般的
  • privateメソッドも呼びだすことができる
  • sendを使って呼び出したい時に動的にメソッドを呼ぶことを「動的ディスパッチ」と呼ぶ
class MyClass
  def my_method(my_arg)
    my_arg * 2
  end
end

obj = MyClass.new
puts obj.send(:my_method, 3) # => 6

動的な定義(動的メソッド)

  • defキーワードではなく、Module#define_methodを用いてメソッドを定義する
  • メソッド名とブロックを渡す必要がある
  • これを「動的メソッド」と呼ぶ
class MyClass
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end

obj = MyClass.new
puts obj.my_method(2) # => 6

Computerクラスのリファクタリング

①動的ディスパッチにより、メソッドを引数として呼び出す

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

②メソッド定義も動的メソッドにする
この時、Computerクラスに対してdefine_componentを呼ぶためにクラスメソッドとしている。

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

③イントロスペクションでメソッド定義も排除
イントロスペクションとはオブジェクトの情報を参照すること。

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

method_missing

  • メソッド探索で継承チェーンを辿っても見つからなかったときにNoMethodErrorを返すメソッド
  • BasicObjectのprivateメソッドだが、動的ディスパッチで呼び出せる
class Lawyer
end

nick = Lawyer.new
nick.send(:method_missing, :my_method)
# => undefined method `my_method' for # (NoMethodError)

ゴーストメソッド

method_missingをオーバーライドすることで存在しないメソッド(ゴーストメソッド)を呼び出せる

class Lawyer
  def method_missing(method, *args)
    puts "呼び出した:#{method}(#{args.join(', ')})"
    puts "(ブロックも渡した)" if block_given?
  end
end

bob = Lawyer.new
bob.talk_simple('a', 'b') do
  # ブロック
end
# =>
# 呼び出した:talk_simple(a, b)
# (ブロックも渡した)

Computerクラスのリファクタリング(再び)

重複問題もゴーストメソッドで処理できる。

しかし、ゴーストメソッドはrespond_to?で認識できないので
respond_to_missingもオーバーライドしないといけないので注意。

class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def method_missing(name)
    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

  def respond_to_missing?(method, include_private = false)
    @data_source.respond_to?("get_#{method}_info") || super
  end
end

computer = Computer.new
computer.mouse

安易なゴーストメソッドは解明しにくいバグを起こす

  • 想定していないメソッド定義は排除する
  • ローカル変数のスコープに注意する
class Roulette
  def method_missing(name, *args)
    person = name.to_s.capitalize
    
    # ↓3つのみゴーストメソッドとして許可する
    super unless %w[Bob Frank Bill].include? person
    number = 0 # ローカル変数のスコープに注意する
    3.times do
      number = rand(10) + 1
      puts "#{number}..."
    end
    "#{person} got a #{number}"
  end
end

number_of = Roulette.new
puts number_of.bob
puts number_of.frank
puts number_of.john

ブランクスレート

  • ゴーストメソッドはmethod_missingであることを前提としている
  • 継承を明記しないとObjectクラスになる
  • Objectクラスには標準メソッドが多く定義されているのでmethod_missingにならないリスクがある

そこで、必要最低限のメソッドしか持たないBasicObjectを継承して回避する。
これを「ブランクスレート」と呼ぶ。

BasicObjectを継承するために、Object#respond_to?を気にする必要がないので、
respond_to_missingのオーバーライドが不要となる利点もある。

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
この記事の内容が役に立ったと思いました、SNSで記事を共有していただけますと幸いです。