Вникаем в include и extend

http://ficate.com/explaining-include-and-extend
  • Перевод

Примечание переводчика: перед прочтением этого поста рекомендую сначала ознакомиться с постом Вникаем в метаклассы Ruby.

Все рубисты знакомы с формальными определениями для include и extend. Вы делаете include модуля, чтобы добавить методы экземпляра класса, и extend — чтобы добавить методы класса. К сожалению, данные определения не совсем точны. Они не могут объяснить почему мы используем instance.extend(Module), чтобы добавить методы объекту. Разве не должны мы в этом случае использовать instance.include(Module)? Чтобы разобраться в этом вопросе, начнем с выяснения где же хранятся методы.

Методы, которые я храню для тебя и парень, который хранит мои методы


Объекты в Ruby не хранят свои собственные методы. Вместо этого они создают синглтон-класс, чтобы он хранил их методы.

class A
  def self.who_am_i
    puts self
  end

  def speak_up(input)
    puts input.upcase
  end
end

Интерпретатор создаст класс A и прикрепленный к нему синглтон-класс (мы будем ссылаться на синглтон-класс объекта используя префикс перед именем объекта). Любые методы экземпляра класса (как speak_up) добавляются к методам, хранящимся в классе A. Методы класса (как who_am_i) хранятся в классе ‘A.

A.singleton_methods # методы в классе ‘A
#=> [:who_am_i]



Тоже самое происходит с экземплярами класса. Если у нас есть объект класса A и мы добавляем к нему метод, мы не можем хранить этот метод внутри самого объекта. Запомните — объекты в Ruby не хранят свои собственные методы.

a = A.new

def a.not_so_loud(input)
  puts input.downcase
end

Здесь опять создается синглтон-класс для объекта "a", чтобы он хранил метод not_so_loud.



Теперь у нас есть метод, который принадлежит только объекту "a" и не затрагивает другие объекты класса A.

Я — свой отец?


Класс A содержит методы и информацию о цепи наследования, нужные для объекта "a" и всех остальных объектов класса A. Точно также синглтон-класс ‘A содержит методы и информацию о цепи наследования для класса A. Вы можете считать класс A объектом класса ‘A. Уловка в том, что мы не можем напрямую обращаться к синглтон-классу ‘A. Это значит, что нам нужно как-то различать добавление методов в A и в ‘A. Тут то include и extend и вступают в игру.

include


Когда вы делаете include модуля в объект, вы добавляете методы в цепь наследования объекта.

class A
  include M
end



В этом легко убедиться, проверив предков класса A.

A.ancestors
#=> [A, M, Object, Kernel, BasicObject]


extend


extend — это тоже самое, что сделать include, но для синглтон-класса объекта.

class A
  extend M
end



И снова мы можем подтвердить это, проверив предков класса ‘A.

A.singleton_class.ancestors
#=> [M, Class, Module, Object, Kernel, BasicObject]

Также мы можем использовать extend и для объекта.

a = A.new

a.extend(M)
a.singleton_class.ancestors
#=> [M, A, Object, Kernel, BasicObject]



Если вы думаете об extend как о способе просто добавить методы класса, то все что мы сейчас сделали не имеет особого смысла. Однако, если вы посмотрите на это как на способ добавить методы к синглтон-классу объекта, то приведенные выше примеры станут яснее.

Хук included


Каждый вызов include проверяет подключаемый модуль на наличие метода included. Этот метод исполняется, когда модуль подключается, используя include. Это как конструктор (initialize) для подключений. Как вы возможно догадались, у extend для этих целей есть собственный метод — extended. Поэтому когда вы хотите добавить сразу и методы класса и методы экземпляра класса, вы можете использовать для этого хук included.

module M
  def self.included(base)
    base.extend(ClassMethods)
  end

  def speak_up(input)
    puts input.upcase
  end

  module ClassMethods
    def who_am_i
      puts self
    end
  end
end

class C
  include M
end

c = C.new

Сначала мы включаем модуль M в цепь наследования класса C.



Затем мы расширяем класс C, добавляя методы в цепь наследования класса ‘C.



Заключение


Когда начинаешь копать глубже типичного использования include и extend, то обнаруживаешь нечто странное и пугающее. Однако стоит понять реализацию лежащую в основе и все сразу обретает смысл. Давайте теперь дадим определение include и extend еще раз.

include — добавляет методы модуля объекту.
extend — вызывает include для синглтон-класса объекта.

Если вас интересует еще больше деталей о том, как работает интерпретатор, то я рекомендую к просмотру выступление Patrick Farley на Ruby Internals.

Примечание переводчика: еще один момент почему мы не можем использовать instance.include(Module) — это то, что метод include является приватным методом класса Module. В целом же до прочтения этой статьи я тоже не так представлял себе работу extend и include, поэтому посчитал стоящим ее перевести.

Внесу еще некоторую ясность: то, что в статье названо «синглтон-класс» (singleton class), имеет и другие названия: метакласс (metaclass) и айгенкласс (eigenclass). Это все одна и та же сущность, для которой в сообществе Ruby пока нет «официального» названия. Я использовал «синглтон-класс», т.к. это ближе к оригиналу. Однако Matz'у (создателю языка) больше импонирует термин eigenclass.
Поделиться публикацией
Комментарии 27
    0
    Спасибо
      0
      Гуд. Еще был перевести магию class << self ;) клянусь, констракт для меня странноватый.
        +1
        Там на самом деле все просто, но если есть необходимость, то найду хорошую статью на эту тему.
          0
          Было бы здорово, если это не статья типа «делай так, и всё будет круто».
            –1
            Де факто, это для меня единственная магия в Ruby. Остальное естественно и понятно.
              +6
              Изучил вопрос. Никакой магии оказывается, действительно вот такой вот синтаксис просто, а означает он как раз доступ к синглтон-классу объекта.

              class Car
                def self.number
                  # ...
                end
              end
              
              # тоже самое, что и...
              
              class Car; end
              class << Car # мы внутри синглтон-класса класса Car
                def number
                  # ...
                end
              end
              
              # тоже самое, что и...
              
              class Car
                # self == Car, поэтому мы снова внутри синглтон-класса Car
                class << self
                  def number
                    # ...
                  end
                end
              end
              

              Или…

              class Car; end
              
              bmw = Car.new
              
              class << bmw # мы внутри синглтон-класса объекта bmw
                def number
                  'abc'
                end
              end
              
              # тоже самое, что и...
              
              def bmw.number
                'abc'
              end
              
              bmw.number #=> 'abc'
              

              Т.е. никакого дополнительного скрытого смысла, просто такой синтаксис. Почему Matz его придумал таким — надо у него спросить :)
                –2
                Подозреваю, что "<<" следует понимать как «на один уровень назад». Синглтон он ведь незаметно встраивается в цепочку наследования, то есть фактически является родителем текущего класса, и вот такой «стрелкой» мы какбы показываем, что идет обращение назад — к предку. Я всегда так себе это представлял.
              0
              Конечно. Статьи типа «делай так» и читать и переводить неинтересно :)
              0
              Мне кажется, что Акжан шутит, ибо будучи ruby-программистом и активным автором в хабы ruby/rails он 100% знает о class << self.
                0
                Не шучу. Я понимаю смысл, но не понимаю синтаксиса (почему именно так?). Мог бы посмотреть, но с первого взгляда не нашёл, а дальше рыть не стал. Работает, и ладно. Лишь иногда возникает ощущение недопонятости.
            0
            Прочитайте главу про метапрограммирование в Thomas D., Fowler C., Hunt A. — Programming Ruby 1.9, updated for Ruby 1.9.2 (The Pragmatic Programmers) — 2010
            Авторы достаточно подробно и понятно объясняют.
              0
              Спасибо, гляну в этом месяце.
            0
            Чего не понятно? Класс — это объект (если не вникать сильно во внутреннее устройство), для объекта мы вызываем метод #extend, чтобы подмешать модуль. А include используется для расширения экземпляров класса. Все понятно и без статьи.
              +1
              Ну как бы из статьи понятно, что все не так уж просто и понятно. Очень многие вещи часто понятны в общих чертах, тем не менее разложенные по полочкам подробности реализации вносят дополнительную ясность в мысли, а кроме того закладывают хороший фундамент для дальнейшего расширения и углубления знаний.
                +1
                Здесь всё-таки подробнее/точнее. Затронута тема сингтонов, не хватает лишь деталей, ибо не все понятно тем, кто уже пишет на Ruby.
                  –4
                  Ну с синглтонами все понятно, да и не вижу смысла разбираться сильно с ними… Не видел еще, чтобы кто-то их непосредственно использовал, то есть осознано. Хотя если просто интересно как оно работает, то да, тогда статья имеет смысл, а не практике — сомневаюсь.
                    –1
                    О! Кто-то люто минусует. Велкам ту май карма!
                +3
                Более понятно и более развернуто это дело описано в книге Metaprogramming Ruby. Очень достойная книга, но не для новичков.
                  0
                  +1 отличная книга и доходчиво написана. Если не ошибаюсь, она на 1.8 ориентирована, но на 99% все еще актуальна.
                    0
                    Странно что там постоянно аналогии с Java приводятся :-) Но книжка хорошая.
                      0
                      Из Java многие на Ruby/jRuby переходят, вот и автор, наверное, один из таких перебезчиков.
                    +2
                    Автор хорошо описал механизм eigenclass'а, возможно для понимания конструкции вида class << self не хватает некоторых знаний о других механизмах языка, вроде Scope Gates.

                    Я крайне советую вам прочитать Metaprogramming Ruby, очень многое станет на свои места media.pragprog.com/titles/ppmetr/spells.pdf Там автор более продробно разбирает механизм (в том числе и с наследованием), приводит еще несколько извращений с eigenclassами.

                    Если нет времени читать всю книгу (а она того стоит!), то вот более подробная статья, www.madebydna.com/all/code/2011/06/24/eigenclasses-demystified.html Статья актуальна для 1.9, в 1.8 не было BasicObject, но кому сейчас нужен 1.8 :)

                    Когда начинаешь копать глубже типичного использования include и extend, то обнаруживаешь нечто странное и пугающее. Однако стоит понять реализацию лежащую в основе и все сразу обретает смысл.


                    Когда начинаешь разбираться, что происходит внутри объекта main, это ощущение возвращается:

                    banisterfiend.wordpress.com/2010/11/23/what-is-the-ruby-top-level/

                      +3
                      Полгода назад писал большую статью на тему объектной модели в ruby — возможно, кому-нибудь пригодится.
                        0
                        А подскажите вот еще какой вопрос. Я написал модуль, в котором переопределил метод .downcase класса String. Но не при include, ни при extend он не вызывается — вызывается стандартный метод объекта String. Исходя из описания, это должно работать при extend, но увы — что-то я не так понял. То есть интересует следующее

                        module M1
                        def downcase
                        return 'Hello!'
                        end
                        end

                        class String
                        include M1
                        end

                        'Привет'.downcase
                        => «Привет»

                        У меня независимо от использования include и extend — всегда возвращается 'Привет'. Как правильно переопределять существующие методы?
                          0
                          Если строка содержит только латинские символы, то все должно работать.

                          Для символов кириллицы попробуйте использовать метод mb_chars (нужен джем 'activesupport'):

                          "Привет".mb_chars.downcase
                          

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

                        Самое читаемое