🎃

Rubyのメタプログラミングで動的にメソッドを操ってみる【method_missing編】

2024/01/29に公開

どうもお疲れ様です。MESIです。
Rubyのメタプログラミングで動的なメソッドの呼び出しと定義方法について学習したので、忘備録として残します。
今回はmethod_missing編です。

前回の記事はこちら
https://zenn.dev/mesi/articles/a45cbf6c1a1165

method_missingとは?

method_missingはRubyにてオブジェクトのメソッド呼び出しが行われた際に、そのオブジェクトが呼び出されたメソッドをもっていない場合に自動的に呼び出されるメソッドです。

class People; end
bob = People.new
bob.talk

# => undefined method `talk' for #<People:0x0000558bbe4796f0> (NoMethodError)

Rubyを使っていたらことのようなエラーを見たことがあるでしょう。
このエラーを呼び出しているのがmethod_missingメソッドです。

method_missingのオーバーライド

method_missingメソッドをオーバーライドすることで、存在しないメソッドに対するカスタムの応答を定義できます。

以下の例では、MyDynamicClassに存在しないメソッドが呼び出された場合、method_missingが動作します。print_で始まるメソッド名の場合はカスタムメッセージを出力し、それ以外は通常のNoMethodErrorを発生させます。

class MyDynamicClass
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?("print_")
      puts "You tried to call #{method_name} with arguments: #{args.join(', ')}"
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?("print_") || super
  end
end

obj = MyDynamicClass.new
obj.print_hello("world")  # => "You tried to call print_hello with arguments: world"
obj.unknown_method        # => NoMethodError: undefined method `unknown_method'

respond_to_missing?メソッドはrespond_to?メソッドに動的メソッドの存在を教えるためにオーバーライドしてます。

Rubyでは、respond_to?メソッドを使ってオブジェクトが特定のメソッドに応答するかどうかを確認できます。
しかし、method_missingを使って動的にメソッドを処理する場合、respond_to?メソッドはデフォルトでこれらの動的なメソッドに対してfalseを返します。
これはrespond_to?がオブジェクトのクラスとそのスーパークラスに定義されているメソッドのみを確認するためです。

この例ではprint_で始まるメソッドに対してrespond_to?がtrueを返すようにオーバーライドしてます。

また、method_missingで処理されたメッセージは呼び出し側からは通常の呼び出しに見えますが、レシーバー側には対応するメソッドがありません。このようなメソッドをゴーストメソッドと言います。

動的プロキシ

動的プロキシとは特定のオブジェクトへのメソッド呼び出しを制御、拡張、または変更するために使用されます。
Rubyでは、この概念は主にmethod_missingメソッドをオーバーライドすることで実現されます。
まずはサンプルコードを見てみましょう。

class DynamicProxy
  def initialize(target)
    @target = target
  end

  def method_missing(method_name, *args, &block)
    if @target.respond_to?(method_name)
      puts "Delegating #{method_name} to target"
      @target.send(method_name, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    @target.respond_to?(method_name) || super
  end
end

class TargetClass
  def hello(name)
    "Hello, #{name}"
  end
end

target = TargetClass.new
proxy = DynamicProxy.new(target)
puts proxy.hello("World")  # => Delegating hello to target
                            # => "Hello, World"

上記の例では、DynamicProxyクラスが任意のオブジェクト(TargetClassのインスタンス)へのプロキシとして機能します。
method_missingを使って、プロキシに存在しないメソッド呼び出しを対象オブジェクトに委譲しています。

どんなときに動的プロキシを使うか?

動的プロキシは、以下のような場面で有効となります。

メソッド呼び出しのインターセプト

他のオブジェクトに委譲する前に、メソッド呼び出しをキャプチャしてログに記録したり、引数を変更したりすることができます。

class LoggingProxy
  def initialize(target)
    @target = target
  end

  def method_missing(method_name, *args, &block)
    puts "Calling method: #{method_name} with arguments: #{args.join(', ')}"
    @target.send(method_name, *args, &block)
  end
end

class MyClass
  def greet(name)
    "Hello, #{name}"
  end
end

obj = MyClass.new
proxy = LoggingProxy.new(obj)
puts proxy.greet("World")  # ここでログが出力される

遅延初期化

オブジェクトが実際に必要になるまでその初期化を遅らせることができます。

class LazyInitializationProxy
  def initialize(&creation_logic)
    @creation_logic = creation_logic
    @object = nil
  end

  def method_missing(method_name, *args, &block)
    @object ||= @creation_logic.call
    @object.send(method_name, *args, &block)
  end
end

class ExpensiveObject
  def initialize
    puts "ExpensiveObject created!"
  end

  def perform
    "Performing a task"
  end
end

proxy = LazyInitializationProxy.new { ExpensiveObject.new }
puts proxy.perform  # ExpensiveObjectはここで生成される

リモートオブジェクトへのアクセス

リモートサーバー上のオブジェクトへのメソッド呼び出しを透過的に処理することができます。

class RemoteProxy
  def initialize(remote_address)
    @remote_address = remote_address
  end

  def method_missing(method_name, *args, &block)
    # リモートサーバーにメソッド呼び出しを送信するロジック
    puts "Sending #{method_name} call with arguments #{args} to #{@remote_address}"
  end
end

remote_object = RemoteProxy.new("http://remote-server.com")
remote_object.do_something("arg1", "arg2")  # リモートサーバーにメソッド呼び出しする

動的な振る舞いの追加

実行時に新しいメソッドや応答をオブジェクトに動的に追加することができます。

class DynamicBehaviorProxy
  def initialize
    @methods = {}
  end

  def add_method(name, &block)
    @methods[name.to_sym] = block
  end

  def method_missing(method_name, *args, &block)
    if @methods.key?(method_name)
      @methods[method_name].call(*args)
    else
      super
    end
  end
end

proxy = DynamicBehaviorProxy.new
proxy.add_method(:greet) { |name| "Hello, #{name}" }
puts proxy.greet("World")  # => "Hello, World"

Discussion