その他
    ホーム 技術発信 DoRuby method_missingを使ってみる
    method_missingを使ってみる
     

    method_missingを使ってみる

    この記事はアピリッツの技術ブログ「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の知識を増やしていこうと思います。