LoginSignup
0
0

More than 3 years have passed since last update.

[学習メモ]メタプログラミングRuby第2版:3章:メソッド

Posted at

概要

メタプログラミングRuby の第3章 メソッド を読んだ学習内容のメモ。

最初にざっくりまとめ

内容的には、メソッド定義における重複コードの問題について解決策を提示する、というもの。

  • 解決方法は大きく2種類のアプローチがある。
    1. 動的メソッド(define_method)
    2. ゴーストメソッド(method_missing)
  • 可能な限りは動的メソッドを使い、仕方のない場合はゴーストメソッドを使いましょう。

例題

「99ドルより高いコンピュータの部品を検出するシステムを作る」という、具体的な例題をベースに進んでいきます。

重複コード問題

このようなコードの重複が多いメソッドをどのように改善していくか...

# 重複がたくさんあるComputerクラス
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

動的メソッドを用いた解決方法

動的ディスパッチ

メソッドの呼び出しに、通常使用するドット記法(obj.my_method(arg))ではなく、 Object#send を使う(obj.send(:my_method, arg))。
send を使うことによって、呼び出したいメソッド名が引数となり、メソッド名を動的に指定する事ができる。
このような手法を 動的ディスパッチ と呼ぶ。

# 動的ディスパッチを利用したリファクタリング
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

動的メソッド

Module#define_method を使うと、メソッドを動的に定義する事ができる。
メソッド名とブロックを渡す必要があり、ブロックがメソッドの本体になる。
ここではComputerクラス定義の中で define_method を呼び出したいので、クラスメソッドにする必要がある。

# define_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

するとこんな風に使える

obj = Computer.new(42, data_source)
obj.mouse # => "Wireless Touch"
obj.price # => 60

さらにインストロペクション

さらに重複を排除するため、data_source をインストロペクション(*)してコンポーネントの名前に展開する。

# data_sourceをインストロペクション!
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
    # get_xxx_infoというメソッドのリストにブロックを渡して、
    # 正規表現にマッチした文字列(mouse, cpu等)の名前のメソッドを定義する
    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

これで、data_source 側にコンポーネントが追加されても、Computerクラスをいじる事なくサポートできるようになった。

(*) インストロペクション ... オブジェクトに対して言語要素(変数、クラス、メソッド等)を質問する事

"hoge".class # classを聞いてみる
=> String
"hoge".methods.grep(/to_(.*)/) # "to_"から始まるメソッドを聞いてみる
=> [: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

存在しないメソッドを呼び出した時、 BasicObject#method_missing が呼ばれる。
よくあるこんな感じ。

class Lawyer; end
nick = Lawyer.new
nick.talk
=> NoMethodError: undefined method `talk' for #<Lawyer:0x00007f921c0f2958>

この method_missing をオーバーライドすることで、実際には存在しないメソッドを呼び出せるようになる。
method_missing で処理されるがレシーバ側には対応するメソッドがない、このようなメソッドを ゴーストメソッド と呼ぶ。

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

  def method_missing(name)
    # @data_sourceに該当のメソッドがなければ親クラスのmethod_missingを呼ぶ
    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

method_missing のバグは潰しにくい

例)

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}"  # ここで無限ループが発生する。なぜでしょう?
  end
end

ブランクスレート

method_missing のもうひとつの罠

先の Computer クラスでは display メソッドだけ正常に動かない。

my_computer = Computer.new(42, DS.new)
my_computer.display # => nil

なぜか。
=> 継承元の Object クラスに display メソッドが既に定義されているから。

Object.instance_methods.grep /^d/
=> [:define_singleton_method, :display, :dup]

Computer#display を呼び出しているつもりが、 Object#display が見つかってしまうため、 method_missing にたどり着かない。

これを解決するには、不要なメソッドは削除された状態にしておく必要がある。
最小限のメソッドしかない状態のクラスを、 ブランクスレート と呼ぶ。

ブランクスレートの実現方法として、BasicObject クラスを継承する方法・不要なメソッドを削除する方法が紹介されている。

結論

このように、ゴーストメソッドは便利だが見つけにくいバグを含む危険をはらんでいる。
で、以下の結論。

「可能な限りは動的メソッドを使い、仕方のない場合はゴーストメソッドを使いましょう。」

今日は家に帰って休息だ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0