Предлагаю вашему вниманию перевод фрагмента книги Metaprogramming Ruby 2 за авторством Паоло Перротта (Paolo Perrotta).

Что такое method_missing?


В Руби мы можем вызывать методы которые не существуют, но это будет возвращать нам ошибку. Для примера:

class Lawyer; end
nick = Lawyer.new
nick.talk_simple

NoMethodError: undefined method 'talk_simple' for #<Lawyer:0x007f801aa81938>

Помните ли вы как работает поиск методов? Когда вы вызываете метод talk_simple, Ruby идет в класс объекта nick, и перебирает там методы. Если он не может найти метод там, он ищет его в родителей данного класса, потом в Object и наконец в BasicObject. Ну и поскольку Ruby не может нигде найти метод talk_simple, он запускает метод method_missing для nick’a. Ruby знает что этот метод есть, потому, что это приватный метод BasicObject от которого наследуются все объекты.

Вы можете сами вызвать для эксперимента этот метод. Поскольку метод приватный, вы может сделать это так:

nick.send :method_missing, :my_method

NoMethodError: undefine method 'my_method' for #<Lawyer:0x007f801aa81938>

Только что вы сделали то что делает Ruby, Вы сказали объекту “Я только что попытался вызвать метод :my_method, а его нет, так что, верни мне ошибку.” BasicObject#method_missing возвращает NoMethodError, по сути для этого он и существует.

Переопределение метода method_missing


Скорее всего вам никогда не нужно будет вызывать метод method_missing самими. Вместо этого вы можете переписать этот метод для перехвата неизвестных вызовов. Каждый такой вызов хранит в себе информацию о названия метода и аргументы которые были переданы.

class Lawyer
  def method_missing(method, *args)
    puts "You called: #{method}(#{args.join(', ')})"
    puts "(You also passed it a block)" if block_given?
  end
end

bob = Lawyer.new
bob.talk_simple('a', 'b') do
# a block
end

You called: talk_simple(a, b)
(You also passed it a block)

Переопределения метода method_missing дает вам возможность вызывать методы которые в реальности не описаны, а то-есть не существуют. Рассмотрим немного детальнее.

Ghost Methods


Когда вам нужно объявить множество подобных методов, спастись от этого поможет вызовов их через method_missing. Это как сказать объекту, что “Если вызывают метод которого не существует, просто сделай это”. Со стороны вызова метода который работает через method_missing, все выглядит как обычный вызов обычного метода, но внутри класса, этого метода просто не существует. Именно этот трюк и называют Призрачным Методом (Ghost Method). Давайте посмотрим на примеры.

Hashie пример


Гем Hashie содержит немного магии под названием Hashie::Mash. Mash это более мощная версия стандартной библиотеки Ruby OpenStruct, это хеш-подобный объект, атрибуты которого работают как переменные Ruby. Если вам нужен новый атрибут, вы просто объявляет значения для данного атрибута. Вот как это работает.

require 'hashie'

icecream = Hashie::Mash.new
icecream.flavor = 'strawberry'
icecream.flavor                        # => 'strawberry'


Это работает потому что Hashie::Mash субклас Ruby Hash, и эти атрибуты по сути являются призрачными методами. Вот как здесь реализован method_missing:

module Hashie
  class Mash< Hashie::Hash
    def method_missing(method_name, *args, &blk)
      return self.[](method_name, &blk) if key?(method_name)
      match = method_name.to_s.match(/(.*?)([?=!]?)$/)
      case match[2]
      when "="
        self[match[1]] = args.first        
        # ...
     
      else
        default(method_name, *args, &blk)
      end
    end

    # ...
  end
end

Если имя вызываемого метода это имя ключа в хеше (как flavor), тогда Hashie::Mash#method_missing просто вызывает [] метод для возврата соответствующего значения. Если же имя заканчивается “=”, тогда method_missing отрезает лишние символы и получает значение которое нужно сохранить. Если же имя не содержит что то другое, тогда method_missing просто возвращает какое-то стандартное значение. Hashie::Mash также содержит и другие специальные символы, например “?”.

respond_to_missing?


Если вы захотите проверить призрачный метод с помощью respond_to?, то понятное дело Ruby вам соврет.

john = Lawyer.new
john.respond_to?(:talk_simple)        # => false

Это может быть проблематично, поскольку мы можем часто использовать respond_to для проверок. Но Ruby обеспечивает хороший механизм что бы подружить respond_to и призрачные методы.

respond_to? вызывает метод respond_to_missing? который возвращает true если это призрачный метод. Для того что бы это не приносило больше проблем при разработке, переопределяйте каждый раз вместе с method_missing и respond_to_missing.

class Lawyer
  # ...

  def respond_to_missing?(method, include_private = false)
    methods.include?(method) || super
  end
end

bill = Lawyer.new
bill.respond_to?(:talk_simple)          # => true

Немного раньше рубисты переопределяли сам respond_to? метод. Но теперь есть respond_to_missing? и переопределение respond_to? рассматривается как не правильный вариант работы с method_missing.

Ghost Methods?—?это лишь малая часть интересных возможностей в Ruby.

Комментарии (28)


  1. Antti
    29.10.2015 18:01

    Нужно еще указать что нужно избегать использования method_missing, потому что это очень плохая практика, мешает отладке, создает плодородную почву для будущих багов.


    1. Comedian
      29.10.2015 21:33

      Выстрелить себе в ногу можно разными способами.

      Я бы не сказал, что method_missing это очень плохая практика, просто его нужно правильно применять, скажем при реализации NullObject.


    1. sc_raptor
      30.10.2015 12:02
      +1

      Вы не поверите, но Rails чуть менее чем полностью это method_missing


      1. Antti
        30.10.2015 12:53

        Почему это не поверю, я так говорю как раз из-за того, что работаю уже 5 лет с Rails и много разного кода с method_missing повидал (да и сам его использовал когда не нужно, чего уж там).


    1. matiouchkine
      30.10.2015 12:55

      Каким это образом он мешает отладке?

      В отсутствие нормального компилируемого AST (как в LISP и/или Elixir), method_missing — единственный способ написать сейчас библиотеку для какого-нибудь развивающегося стандарта, наподобие UTF, так, чтобы она на лету подхватывала изменения без участия программиста.

      Заученная мантра про «это плохо» — сразу выявляет людей, который просто не понимают, как устроен руби.


      1. Antti
        30.10.2015 13:17
        +1

        Это не мантра, а рекомендация. Статья рассчитана на новичков, у которых как раз и возникает соблазн использовать method_missing не по назначению, не понимая как можно избежать его использования.
        Большинство языков не имеют поддержки «method_missing», что не мешает использовать их для написания всяческих библиотек.
        И я не понимаю при чем здесь «компилируемый AST», и что это за такой стандарт UTF, которий развивается.


        1. matiouchkine
          30.10.2015 14:25
          -1

          На простой вопрос вы не ответили; сами подтвердили, что мало, что понимаете, и, тем не менее, раздаете рекомендации.

          Мило.


          1. Antti
            30.10.2015 16:39
            +1

            Во первых я не подтверждал что мало понимаю, это вы так решили,
            во вторых, отладке оно мешает хотя бы по тому что найти вызываемый метод в коде нереально, и приходится трейсить програму и смотреть где что вызывается.

            Спорить с вами не хочу, потому что вы переходите на личности, и также не отвечаете на мои вопросы.


            1. matiouchkine
              30.10.2015 18:24
              -1

              > я не подтверждал что мало понимаю
              >> я не понимаю при чем здесь «компилируемый AST», и что это за такой стандарт UTF, которий развивается

              > также не отвечаете на мои вопросы
              Да я бы ответил с удовольствием, если вы бы хоть один вопрос задали.

              Спорить со мной не нужно, это разумное решение. Но и громкоми беспочвенными неумными прокламациями под видом «рекомендаций» бросаться тоже не сто?ит.


              1. Antti
                30.10.2015 18:34
                +1

                что это за такой стандарт UTF, которий развивается?

                Вы напишите конкретный пример, когда без method_missing не обойтись, тогда и поговорим.

                А все что вы написали выше, выдает у вас невежу, который только и может что бросаться словечками которые он даже не понимает.


                1. matiouchkine
                  30.10.2015 20:25
                  -1

                  О, да вы не отличаете вопросительное предложение от утвердительного. Тогда все ясно. И еще, на будущее: передергивать некрасиво, даете цитату — не нужно приукрашивать ее до неузнаваемости.

                  У вас в голове такая каша, что вы каждым словом палитесь: «когда без method_missing не обойтись». Обойтись можно без чего угодно, в ассемблере, вон, вообще все очень аскетично. Вопрос в другом: где имеет смысл использовать method_missing?..

                  Отвечу, что ж, раз обещал. Может, кто неглупый придет эту ветку почитать.

                  В любом динамически расширяемом классе (например, в библиотеке, которая работает с UTF, который, в свою очередь, каждый день дополняется консорциумом, а спецификация специально лежит в наборе постоянно изменяемых текстовых файлов, которые принято парсить, чтобы быть в ногу со временем). Вы, наверное, уже приготовились мне сказать про define_method — я слыхал, что такой есть — вот только использовать его нужно только если количество потенциальных вызовов заметно превышает единицу в медиане. Иначе это очень дорого: на каждый чих расширять класс. Англичане в таких случаях говорят «performance penalty» и больно бьют по голове.

                  Или, к примеру, DSL для lookup”а пользователя в ActiveRecord по имени: `User.matiou`, который возвращает пользователя[-ей] с таким логином, или почтой. Или еще миллион примеров.

                  Если вдруг понадобится еще какой тривиальный пример чего-нибудь — не стесняйтесь, жгите.


                  1. Metus
                    30.10.2015 20:50
                    +1

                    Несмотря на всё это, method_missing это не единственный способ. В Вашем примере с UTF он лучше, но не единственный.
                    И всё это не отменяет недостатка в виде лапши в stack trace.

                    Ваше мнение, на мой взгляд, слишком категорично, а его изложение изобилует переходами на личности.


                    1. matiouchkine
                      30.10.2015 21:27
                      -1

                      В любом языке программирования, включая Оберон, есть минимум два способа «сделать это». Я сказал «единственный» в полемическом запале, имея в виду «единственно приемлемый». Прошу благосклонно простить словоблудие :)

                      Что же до моего метода изложения — да, все так, я им доволен, и в ближайшее время менять не собираюсь. Выражаясь менее высокопарно, я срать хотел на мнение общественности о моем методе изложения, простите.


                1. drakmail
                  31.10.2015 11:07

                  Вы напишите конкретный пример, когда без method_missing не обойтись, тогда и поговорим.


                  Я могу написать — dsl для работы с rest api, работа с xml, yaml, json, работа с СУБД — всё это становится гораздо приятнее с method_missing.


      1. Metus
        30.10.2015 14:21

        Плохо это тогда, когда у вас в к какому-то классу подключено 5 модулей и каждый с method_missing.
        А ещё вам придётся определять respond_to?


        1. matiouchkine
          30.10.2015 15:26

          Да ну? Прям плохо? Чем же?

          В спецификации четко написано все про иерархию. Если лень читать про иерархию, можно `ancestors` позвать на классе. Если каждый модуль написан нормально, они позовут `super` на необработанные варианты, и у вас забесплатно получится самая удобная система плагинов, какую только можно себе представить.

          Ну и `respond_to?` не рекомендовано переопределять уже года два как. http://stackoverflow.com/a/20731357/2035262

          Без владения матчастью-то все плохо, даже method_missing.


          1. Metus
            30.10.2015 16:14

            Плохо тем что будет длинная цепочка через super. Чисто эстетически это плохо, т.к. делает stack trace необоснованно длинным. Не могу сказать, что это сильно затрудняет отладку, но точно добавляет мусор.

            respond_to? или respond_to_missing? — не важно что вы переопределите — вам придётся делать это, т.е. переопределять сразу 2 метода.

            Систему плагинов в ряде случаев можно сделать точно также, но с использованием define_method — и не нужно ничего переопределять.

            В некоторых случаях, конечно, без method_missing не обойтись, но весь посыл в том, чтобы использовать его только когда это необходимо.


            1. matiouchkine
              30.10.2015 16:37

              > посыл в том, чтобы использовать его только когда это необходимо
              Это прекрасный девиз, для _любого всего, чего угодно_; я с ним полностью согласен.

              Заявление же «нужно избегать использования method_missing», с которым я тут в этой ветке дискутирую, по степени осмысленности конкурирует с «нужно избегать пользоваться утюгом [, потому что им неудобно заколачивать шурупы]».


  1. justnoxx
    29.10.2015 18:15

    Очень похоже на перловый AUTOLOAD.


  1. conf
    29.10.2015 19:55
    +1

    Книга превосходная, но ваш перевод довольно корявый и содержит очень много опечаток. Например, в первом листинге кода 2 ошибки, должно быть вот так:

    class Lawyer; end
    nick = Lawyer.new
    nick.talk_simple
    
    NoMethodError: undefined method 'talk_simple' for #<Lawyer:0x007f801aa81938>
    


    1. xo8bit
      29.10.2015 20:03

      Да, спасибо, исправил опечатку. Я немного изменил в конце, что бы не переводить всю главу.


      1. j_wayne
        30.10.2015 13:21
        +1

        Во втором блоке тоже, должно быть вот так:

        nick.send :method_missing, :my_method
        
        NoMethodError: undefine method 'my_method' for #<Lawyer:0x007f801aa81938>
        


        1. xo8bit
          30.10.2015 13:24

          Спасибо, исправил!


  1. Metus
    29.10.2015 22:18
    +2

    Я бы добавил, что единственное более менее оправданное использование method_missing это когда ваш метод может в любой момент времени существовать или нет — как на примере хэша.

    В случае же если это делается из удобства, например хотите прописать в классе одну строчку вроде

    mount_auth_for :admin

    и получить методы вроде
    current_admin
    sign_in_admin(admin_model)
    sign_out_admin
    

    , то в таком случае мне ближе подход через define_method. В таком случае будет создан полноценный метод.


    1. fuCtor
      30.10.2015 06:37
      +1

      Хорошей практикой при использовании method_missing является объявление в нем новых методов, для оптимизации вызовов, при условии что этот метод будет постоянен. Например как в финдерах у AR (в 4ой версии перешли на немного другой синтаксис).


      1. Antti
        30.10.2015 18:38

        Согласен, да и то, наверное лучше сделать методы динамически при определении класса, а не при использовании.


  1. j_wayne
    30.10.2015 13:25
    +2

    Ghost Methods?—?это лишь малая часть интересных возможностей в Ruby 2

    Позвольте заметить, что method_missing был в ruby всегда.
    То есть, это не исключительная возможность Ruby 2, как это может кому-то показаться из утверждения.


  1. Antti
    30.10.2015 18:46

    Интересно что хоть method_missing и обьявлен в BasicObject, его можно оттуда удалить и все все-равно будет работать:

    irb(main):002:0> BasicObject.respond_to?(:method_missing, true)
    => true
    irb(main):004:0> BasicObject.send(:undef_method, :method_missing)
    => BasicObject
    irb(main):005:0> BasicObject.respond_to?(:method_missing, true)
    => false
    irb(main):008:0> foo = Object.new
    => #<Object:0x007fa1c2831470>
    irb(main):009:0> foo.bar
    NoMethodError: undefined method `bar' for #<Object:0x007fa1c2831470>