読者です 読者をやめる 読者になる 読者になる

気軽に楽しくプログラムと遊ぶ

自分が興味があってためになるかもって思う情報を提供しています。

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

動的メソッド

メソッド名、引数値を指定して、メソッドを呼び出す。 これで重複コードをリファクタしたりする。

class MyClass
  def my_method(my_arg)
    my_arg * 2
  end
end

obj = MyClass.new()
# 普通のメソッド呼び出し
obj.my_method(2)
=> 4

#動的メソッド呼び出し
obj.send(:my_method, 2)
=> 4

#動的メソッド呼び出し(存在しないメソッド名を読んだ場合)
obj.send(:my_method1, 2)
NoMethodError: undefined method `my_method1' for 

send(メソッド名,引数)の形式でメソッドを呼び出せる。 javaのリフレクションと比べるとめちゃ簡単に使える。 いくつもオブジェクト作ったり、例外捕捉したりがいらない。。お手軽。

オブジェクトのプライベートメソッドを覗いてみよう

class MyClass
  private
  def my_private_method
    puts "private method"
  end
end

obj = MyClass.new()

#通常のレシーバーを用いての呼び出しはできない
obj.myprivate_method
NoMethodError: undefined method myprivate_method

# sendを使った動的呼び出しだと。見える。わぁお。
obj.send(:my_private_method)
#=> private method

メソッドを動的に定義する

class MyClass
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end

obj = MyClass.new()
obj.my_method(3)
=> 9

処理上でメソッドが作れるようになった。rubyすごい。。

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
end

内部処理をメソッド
内部のメソッド呼び出しを抽象化することでメソッド化を可能にしている

def mouse
  component :mouse
end

def cpu
  component :cpu
end


def component(name)
  # Object#sendを用いて動的メソッド呼び出しでメソッド呼び出しを抽象化
  info = @datasource.send "get_#{name}_info", @id
  price = @datasource.send "get_#{name}_price", @id
  # #{}内でも関数が使える。#{}は変数やメソッドを解釈できる
  result = "#{name.to_s.capitalize} : #{info} ($#{price)}"
  return "* #{result}" if price >= 100
  result
end

個人的に気になったのはシンボルを文字列に埋め込むと文字列に自動変換されている箇所
一応試してみたが、問題なく変換されている

name = :AAA
=> :AAA
puts "get_#{name}_info"
get_AAA_info

メソッド生成も動的生成にする

Module#define_methodを用いて与えられた名前のメソッドを定義

# クラス内で呼び出すためにクラスメソッドとして定義
def self.define_component(name)
  define_method(name) {
    info = @datasource.send "get_#{name}_info" , @id
    ・・・
  }
end

difine_component :mouse
difine_component :cpu

コードイントロスペクションを用いて、メソッド定義を排除。

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 }
    
    def self.define_component(name) {
    ・・・

grepでマッチした(.*)の部分$1 を用いてブロック内の処理を行うことで メソッド定義が更にシンプル化。。うーむ。すごい。

ちなみにdata_source.methodsの部分がコードイントロスペクション。 オブジェクト内の情報(今回はメソッド)を参照することをコードイントロスペクションと呼ぶらしい。

method_missingについて

・BasicObjectのインスタンスメソッド
・method_missingはメソッド呼び出し時に継承チェーンを上がって調査し、Object・Kernelになかったら元レシーバーのmethod_missing()を呼び出し、NoMethodErrorを出力する。

直接、method_missingを呼び出してみる

myClass.send :method_missing, :my_method
NoMethodError: undefined method `my_method` for 

method_missingをオーバーライドしてみる

実際には存在しないtalk_simpleメソッド(ゴーストメソッド)を呼び出せ、
メソッド名、引数情報、ブロックが存在するかなどの情報を活用できる。

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

bob = Lawyer.new
=> #<Lawyer:0x007fc4c90c6d50>

bob.talk_simple('a','b') do
  "ブロック"
end

talk_simple(a,b)を呼び出した
(ブロックを渡した)

OpenStructクラスでゴーストメソッドを体験する

require 'ostruct'

icecream = OpenStruct.new
=> #<OpenStruct>

# 存在しない、flavor=()メソッドを呼び出せる
icecream.flavor = "ストロベリー"

# 存在しない、flavorメソッドを呼び出せる
icecream.flavor
=> "ストロベリー"

OpenStructの処理はどうなっているんだろう。
簡単な内部処理をシンプルに書くと以下のようになるそう。

class MyOpnenStruct
  def initialize
    @attributes = {}
  end
  
  def method_missing(name, *args)
    attribute = name.to_s
    # =〜な名称ならばハッシュに=を削除したKeyで引数に設定された値を設定
    if attribute =~ /=$/
      @attributes[attribute.chop] = args[0]
    # =がつかないメソッド名の場合は取得名称をKeyとしてハッシュより値取得
    else
      @attributes[attribute]
    end
  end
end

icecream = MyOpenStruct.new
icecream.flavor = "バニラ"
icecream.flavor # => "バニラ"

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

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})"
    #{result}" if price >= 100
    result
  end
end

method_missingを用いてリファクタリング

class Computer
  ・・・
  method_missing(name, *args, )
    # 呼び出しメソッドが存在確認。存在しない場合、元のmethod_missingの処理を呼ぶ
    super if !@data_source.respond_to?("get_#{name}_info")
    # メソッド名を元に動的ディスパッチ(呼び出し)を行う。
    info = @data_source.send("get_#{name}_info" ,arg[0])
    price = @data_source.send("get_#{name}_price" ,arg[0])
    result = "#{id.capitalize}: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
end

com = Computer.new()
com.mouse
com.cpu

get_mouse_infoなどのゴーストメソッドはrespond_to?しても存在しないと言われる。

これを回避するにはrespond_to?をクラス内で上書きして、 method_missing内で呼ばれるメソッドの存在チェックをするとよい。

# メソッド上書き前
com.respond_to?(:mouse) # => false

class Computer
  def respond_to?(method)
    @data_source.respond_to?("get_#{method}_info") || super
  end
・・・

# メソッド上書き後
com.respond_to?(:mouse) # => true

メソッド名の衝突を回避せよ

method_missingで呼び出す予定のメソッドが既存ですでに定義済みの場合がある。
その場合は存在するメソッドが呼ばれてしまう。これを回避するには以下

class Computer
  instance_methods.each do |m|
    #パターンに示したメソッド以外のObjectから継承したメソッドを削除
    undef_method m unless m.to_s =~ /^__|method_missing| respond_to?/
  end

Objectから継承したメソッドをすべて削除したブランクストレートを継承するとメソッド名の衝突を回避できる。上記例は、一部メソッドを残しているので、純粋なブランクストレートではない。

Ruby1.9からブランクストレートが言語に組み込まれた。 クラスルートのBasicObjectである。これは必要最低限のメソッドしかないクラス。

BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]

BasicObjectを直接継承したクラスはブランクストレート(Object内のインスタンスメソッドを継承していないクラス)となる。

次はブロック
メタプログラミング Ruby 第3章 ブロック - 気軽に楽しくプログラムと遊ぶ