Rubyのメタプログラミングで動的にメソッドを操ってみる【method_missing編】
どうもお疲れ様です。MESIです。
Rubyのメタプログラミングで動的なメソッドの呼び出しと定義方法について学習したので、忘備録として残します。
今回はmethod_missing編です。
前回の記事はこちら
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