Madogiwa Blog

主に技術系の学習メモに使っていきます。

Rubyのmethod_missingの使い方MEMO🔯

最近Rubyのメタプロ本を再読して、method_missingを使った実装を試してみたので使い方とかメモしておきます📝

method_missingとは?

呼びだされたメソッドが定義されていなかった時、Rubyインタプリタがこのメソッドを呼び出します。 https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/method_missing.html

method_missingとはRubyリファレンスマニュアルに記載してあるとおり、呼び出したメソッドが定義されていなかったときに自動的に呼び出されるメソッドです。

class Hoge
 def method_missing(name, *args)
   puts "method #{name} is missing... :("
 end
end

irb(main):040:0> Hoge.new.fuga
method fuga is missing... :(
=> nil

このような感じでメソッドが見つからなかったときの挙動を定義できます。 これを使うと実行時に定義するメソッドを動的に決めたり色々なことができます。

method_missingを使ってみる

引数で渡された複数のClientの挙動を引き継いで独自の実装を追加するようなClassが必要となったときmethod_missingが役に立ちます。 例えば複数のDBMSに対して同一の処理を実行するために、それぞれのDBMSのクライアントの挙動を引き継ぎながら共通の処理を追加するWrapperなどです。

※Clientの挙動の引き継ぐやり方として、継承を使うことも考えられますが複数のClientに対応することはできません。 ※またRuby標準のSimpleDelegatorを使ったやり方では複数のClientの挙動を委譲することはできません。

class ClientsWrapper
  def initialize(clients:)
    @clients = clients
  end

  def method_missing(name, *args)
    @clients.each { |client| client.send(name, *args) }
  end
end

class Mysql
  def self.exec
    puts "Mysql"
  end
end

class Sqlite
  def self.exec
    puts "Slite"
  end
end

irb(main):016:0> ClientWrapper.new(client: Client).exec
Mysql
Slite

このような形で複数のClientに対して対してメソッドを実行することができました!

method_missingを使ってもrespond_to?をtrueにしたい

method_missingを使うと動的にメソッドを定義出来る一方、respond_to?を使って挙動を制御するといったことはできなくなってしまいます😢

irb(main):082:0> ClientsWrapper.new(clients: [Mysql,Sqlite]).respond_to?(:exec)
=> false

この問題を解決してくれるのがrespond_to_missing?です。

自身が symbol で表されるメソッドに対し BasicObject#method_missing で反応するつもりならば真を返します。 Object#respond_to? はメソッドが定義されていない場合、デフォルトでこのメソッドを呼びだし問合せます。 https://docs.ruby-lang.org/ja/latest/method/Object/i/respond_to_missing=3f.html

このメソッドを使うとrespond_to?の挙動を制御してmethod_missingによる応答の場合でもrespond_to?をtrueにするといったことができます。

先程のClientsWrapperclientsに定義されているメソッドだったら実行可能なので、respond_to?の結果がtrueになるように修正してみます。

class ClientsWrapper
  def initialize(clients:)
    @clients = clients
  end

  def method_missing(name, *args)
    @clients.each { |client| client.send(name, *args) }
  end

  def respond_to_missing?(sym, include_private)
    # 各clientのpublic_methodsに呼び出したメソッドが含まれていればtrueを返す。
    @clients.map(&:public_methods).flatten.include?(sym) ? true : super
  end
end

respond_to?(:exec)を実行したときにtrueが返却されています🙌

irb(main):109:0> ClientsWrapper.new(clients: [Mysql,Sqlite]).respond_to?(:exec)
=> true

おわりに

今回はrubymethod_missingについて記載してみました。method_missingを使うと柔軟にメソッドを定義することができますが、

「With great power comes great responsibility.(大いなる力には、大いなる責任が伴う)」 スパイダーマン - Wikipedia

ということで使い所には注意していきたいですね😅

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版