0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rubyのメソッドを操る!動的ディスパッチ!動的メソッド!そして、ゴーストメソッド!

0
Posted at

Rubyのメソッドを操作し、コードの重複を排除する

概要

この記事は、メタプログラミングRuby 第2版の第3章「メソッド」を読み学習した内容を個人学習用にまとめ直したものである。

本記事では、Rubyのメソッドの挙動を操り、コードの重複をリファクタリングする様々な手法について解説しています。

動的メソッド

動的ディスパッチ

sendメソッドを使うと、メソッド名を文字列やシンボルで指定して呼び出せる。
第2引数以降は、そのままメソッドの引数として渡される。

class Calculator
  def add(a, b)
    a + b
  end

  def greet(name, message: "こんにちは")
    "#{message}#{name}さん"
  end
end

calc = Calculator.new
calc.send(:add, 10, 5) # => 15
calc.send(:greet, "太郎", message: "おはよう") # => "おはよう、太郎さん"

# メソッド名を動的に指定
method_name = "add"
calc.send(method_name, 3, 7) # => 10

**通常のドット記法でなくsendを使用すると、呼び出したいメソッドが通常の引数となり、コードの実行時に呼び出すメソッドを決定することができる(動的ディスパッチ)。
**

動的ディスパッチの例:

gems/pry-0.9.12.2/lib/pry_instance.rb
def refresh(options={})
  defaults = {}
  attributes = [ :input, :output, :commands, :print, :quiet, :exception_handler, :hooks, :custom_completions, :prompt, :memory_size, :extra_sticky_locals]

  attributes.each do |attribute|
    defaults[attribute] = Pry.send attribute # 複数のメソッド呼び出しを一行で
  end
end

動的メソッド

define_methodを使うと、実行時にメソッドを動的に定義できる(動的メソッド)。

class MyClass
  define_method :my_method do |my_arg| # メソッド名とブロックを渡す
    my_arg * 3
  end
end

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

動的メソッドにより、実行時にメソッド名を決定することができる。

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

    # DSモデルのオブジェクト。get_#{name}_info or priceでデータを取得
    @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

method_missing

オブジェクトに存在しないメソッドを呼び出すと、継承チェーンを辿って探索が全て終了した後に、全てのオブジェクトが継承するBasicObjectのprivateメソッドであるmethod_missingが呼ばれ、NoMethodErrorが出力される。

nick.my_nonexistent_method

# => NoMethodError

ゴーストメソッド

method_missingをオーバーライドすれば、存在しないメソッドを呼び出した時の挙動を制御することができる。

さらに、**method_missingの中で存在しないメソッドを、呼び出し側からは通常のメソッド呼び出しのように見せる処理をすることも可能となる(ゴーストメソッド)。
**

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

  # 継承元のBasicObjectのmethod_missingをオーバーライド
  def method_missing(name, *args)
    # respond_toがtrueの時、挙動を書き換える。falseの時はError
    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

my_computer = Computer.new(42, DS.new)

# クラスに定義していないcpuメソッドが通常通り結果を返す
my_computer.cpu # => * Cpu: 2.9 Ghz quad-core ($120)

respond_to_missing?

上記の状態だと、例えばcpuメソッドを呼び出すと通常通り結果が返ってくるが、respond_to?メソッドの返却はfalseのままとなる。

cmp = Computer.new(0, DS.new)
cmp.respond_to?(:cpu) # => false

これはrespond_to?メソッドが、メソッドがゴーストメソッドであった時にtrueを返却するrespond_to_missing?というメソッドを呼び出しているため。

つまり、respond_to?の結果を、ゴーストメソッドを設定した際の実際の挙動と一致させるために,method_missingをオーバーライドした際にはrespond_to_missing?もオーバーライドする必要がある。

class Computer
  #...

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

cmp.respond_to?(:cpu) # => true

ゴーストメソッドのバグは発見しづらい

以下のように、method_missingの中でバグを発生させてしまうと、本来出力されるべきNoMethodErrorが出力されないのでバグの原因に気付きづらくなる。

class Roulette
  def method_missing(name, *args)
    person = name.to_s.capitalize
    3.times do
      number = rand(10) + 1
      puts "#{number}..."
    end

    # timesブロックの中で定義しているnumberを呼び出している
    # numberはselfのないメソッド呼び出しと判断され、何度もmethod_missingが呼ばれる
    "#{person} got a #{number}"
  end
end

number_of = Roulette.new
puts number_of.bob

# => 2... 
# => 7... 
# => 1... 
# => 5... 
# ...
# => SystemStackError

このようなバグを防ぐために、大前提として、必要のないゴーストメソッドは導入しないこと。

ブランクスレート

ゴーストメソッドを使う際、継承した既存メソッドと名前が衝突することがある。

class Tag
  def method_missing(name, *args)
    name.to_s # タグ名を返したい
  end
end

Tag.new.display # => "#<Tag:0x00007f>"  ← Object#displayが呼ばれてしまう

このような衝突を防ぐために、継承メソッドを最小限に削ぎ落としたクラスをブランクスレートと呼ぶ。

BasicObjectを継承する

BasicObjectはRubyの最上位クラスで、メソッドがほとんど定義されていない。これを継承するのが最もシンプルなブランクスレートの作り方である。

class Tag < BasicObject
  def method_missing(name, *args)
    name.to_s
  end
end

Tag.new.display # => "display"  ← ゴーストメソッドが正しく呼ばれる

undef_methodで個別に削除する

BasicObjectを継承せず、特定のメソッドだけを削除したい場合はundef_methodを使う。

class Tag
  undef_method :display # Object#displayを削除

  def method_missing(name, *args)
    name.to_s
  end
end

Tag.new.display # => "display"  ← ゴーストメソッドが正しく呼ばれる

undef_methodremove_methodの違い

  • undef_method: メソッドを完全に未定義にする(スーパークラスのメソッドも呼ばれなくなる)
  • remove_method: そのクラスのメソッド定義のみ削除する(スーパークラスのメソッドは呼べる)

ブランクスレートでは継承メソッドも含めて削除したいため、undef_methodを使う。

まとめ

  • sendで動的にメソッドを呼び出せる(動的ディスパッチ)
  • define_methodで動的にメソッドを定義できる(動的メソッド)
  • method_missingをオーバーライドするとゴーストメソッドを実装できる
  • respond_to_missing?method_missingとセットで定義する
  • ゴーストメソッドの名前衝突を防ぐにはブランクスレートを使う

動的メソッドを使うか、ゴーストメソッドを使うかの目安:

「可能であれば動的メソッドを使い、仕方なければゴーストメソッドを使う。」

参考文献

この記事は以下の情報を参考にして執筆した。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?