この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
method_missing とは
何かメソッドを呼び出したとき、
rubyはそのクラスに該当メソッドがあるか捜索し、なければ継承元のクラスを捜索しに行きます。
これはごく普通の挙動ですよね。
ですが捜索した結果該当メソッドが存在しなかった場合呼び出されるのが method_missing
です。
これをうまいこと使うとコードが一気に短縮できるのでそのパターンを紹介します。
実際以下のようなクラスがあった場合を想定します。
class User
def update
puts "更新されたよ"
end
def login
puts "ログインしたよ"
end
# more methods...
end
class UsersManager
def initialize
@array_users = []
end
def add_user(user)
@array_users << user
end
def update
@array_users.each do |user|
user.update
end
end
end
UsersManagerが持っている配列内のUserのメソッドをまとめてコールするというだけのものです。
manager = UsersManager.new
manager.add_user(User.new)
manager.update
=> "更新されたよ"
問題なく使用できると思います。
ですが今の状態で下記のように実行すると
manager.login
NoMethodError: undefined method `login'
もちろんエラーになります。
単純に考えるとUserManagerにloginの処理を追加すれば実行できますが、Userが100や150のメソッドを持っていてそれらも実行しようとしたとき、その数分メソッドを作るのは現実的じゃないですよね。
method_missingを利用してみよう
上の実行ではNoMethodErrorが出力されています。
rubyでは呼び出されたメソッドが存在しなかった場合に
manager.send(:method_missing, :login)
が呼ばれ、その結果NoMethodErrorになります。
これがどうかしたのかというと、あくまでただのメソッドなので
継承先に method_missing を実装してやることで挙動を制御することができます。
先ほどのクラスに method_missing を下記のように実装して実行すると、
class UsersManager
def initialize
@array_users = []
end
def add_user(user)
@array_users << user
end
def method_missing(name, *args)
@array_users.each do |user|
user.send( name, *args )
end
end
end
manager = UsersManager.new
manager.add_user(User.new)
manager.login
=>ログインしたよ
という形になります。
これは呼び出したメソッドがUserManagerクラスに存在しなかったので method_missing が呼び出されるのですが、その時UserManagerクラスに method_missing がないか捜索してくれるのでこのような実行結果になるわけです。
こうすればUserクラスにどれだけメソッドが増えたとしてもUserManagerには処理を追加する必要がなくなります。
とっても便利ですよね。
使用時の注意点
無限ループに陥りやすい?
method_missingを記述するとき、「継承元にあるmethod_missingは呼ばれない」
ということに気をつけなければいけません。
例えば先ほどのUserManagerクラスを以下のように変更します
def method_missing(name, *args)
self.hoge
@array_users.each do |user|
user.send( name, *args )
end
end
この状態でmethod_missingが呼ばれると
manager.login
=>output error: SystemStackError: stack level too deep
どうやらStackOverFlowが起こっているようです。
self.hogeが存在しない -> method_missingが呼ばれる -> self.hogeが存在しない -> ……
といった風に無限ループを発生させてしまっています。
今回のパターンではかなり露骨な書き方をしているのでわかりやすいですが、NoMethodErrorは比較的やってしまいがちなエラーだと思います。それがいきなりStackOverFlowになってしまうので、この危険性は頭の隅に置いておかないと何のエラーだかわかりにくく危険です。
処理を追いにくい
method_missingを使用したコードを他の人が追おうとした時、クラスに呼んだメソッドが記述されていなかったらまず親クラスのメソッドなのかと疑うと思います。そもそもmethod_missingを知らない人はどうしようもないですよね。
まとめ
実際うまく使えたらとっても便利だと思いますが、method_missingを知っておかないと何をしているのか分からないコード
だなという印象を強く感じ、
こういった特殊な動きをするものは知識として入れておかないといけないなと再認識できました。
これから先難解なコードに手をつける機会が訪れた時に迷うことを極力減らせるように、rubyの知識を増やしていこうと思います。