Модули — это удивительно гибкие языковые конструкции, которые можно применять для самых разных вариантов использования, таких как пространство имен, наследование и декорирование. Однако некоторые разработчики все еще не понимают, как работают модули и как они взаимодействуют со своим собственным кодом. Эта статья призвана пролить свет на модули и их использование.
Объектная модель Ruby (ROM)
Как и в большинстве вещей в Ruby, модули очень просты в использовании, как только мы поймем, как они вписываются в объектную модель Ruby (ROM). ПЗУ заполняется объектами, которые являются классами ( class-objects , aka Classes) и объектами, которые являются экземплярами классов ( instance-objects ). Ох, и модули тоже! Давайте создадим пару классов:
class Car def drive "driving" end end class Trabant < Car end
Если мы визуализируем ПЗУ на 2D-плоскости, иерархия объектов выглядит следующим образом:
Класс-объекты нашего класса получены из живых выше нашего класса. Объекты класса, производные от нашего класса, живут под ним. Справа от нашего класса живет его собственный класс (он же синглтон-класс или мета-класс). Собственные классы тоже являются объектами класса, и поэтому они имеют суперклассы, обычно собственный класс суперкласса нашего класса или, в случае объектов экземпляра, класс самого объекта. Полное описание собственных классов выходит за рамки данной статьи и заслуживает отдельной статьи. Пока не беспокойтесь о них, просто знайте, что они есть и у них есть цель.
Итак, что происходит, когда мы создаем объект на основе нашего класса?
my_car = Car.new
Ruby создаст собственный класс нашего объекта прямо под классом Car. Он разместит наш новый экземпляр ( my_car
) слева от своего собственного класса и за пределами иерархии объектов.
На самом деле мы можем проверить все это в Ruby’s irb
:
my_car.class => Car > my_car.singleton_class => #<Class:#<Car:0x0000000165e568>> > my_car.singleton_class.class => Class > my_car.singleton_class.superclass => Car
Поиск метода
Давайте назовем метод для нашего объекта?
my_car.drive
Объект, для которого мы вызываем метод ( my_car
), называется объектом- получателем . У Ruby очень простой алгоритм поиска методов:
приемник -> смотреть вправо -> смотреть вверх
Когда мы вызываем метод drive
для my_car
, Ruby сначала смотрит вправо в my_car
класса my_car. Он не найдет определение метода там, поэтому он будет искать объекты Car, где он найдет определение drive
. Знание этого делает понимание того, как легко использовать модули.
Модули в их среде
Есть три вещи, которые мы можем сделать с модулем:
- Включите его в наши класс-объекты
- Добавьте его к нашим объектам класса
- Расширьте наши объекты класса или объекты экземпляра с этим
Правило вертолета
Руби слотов Модули в ПЗУ, следуя тому, что я называю правилом Вертолет. Я называю это так, потому что в моем искривленном воображении диаграмма выглядит как вертолет, как показано на рисунке выше. Правило вертолета означает:
- Включение модуля в наш объект поместит его прямо над нашим объектом.
- При добавлении модуля к нашему объекту он будет размещен непосредственно под нашим объектом.
- Расширение нашего объекта модулем поместит его прямо над собственным классом нашего объекта.
Давайте проверим это в коде:
module A; end module B; end module C; end class MyClass include A prepend B extend C end MyClass.ancestors => [B, MyClass, A, Object, Kernel, BasicObject] MyClass.singleton_class.ancestors => [#<Class:MyClass>, C, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
Мы видим, что модуль A размещается непосредственно над MyClass
(входит в комплект), модуль B размещается непосредственно под MyClass
(добавляется), а модуль C размещается непосредственно над собственным классом MyClass
(расширенный)
ПРИМЕЧАНИЕ. Вызов метода суперкласса для объекта класса даст нам следующий более высокий класс в цепочке предков выше нашего класса. Однако, поскольку модули технически не являются классами, superclass
не будет показывать нам какие-либо модули прямо над нашим классом. Для этого нам нужно использовать метод ancestors
.
В том числе
Давайте откроем наш класс Car и добавим модуль:
module V12Engine def start; "...roar!"; end end class Car include V12Engine end
Когда мы включаем модуль в наш класс, Ruby размещает его прямо над нашим классом. Это означает, что когда мы вызываем метод для нашего экземпляра объекта, Ruby находит его в иерархии объектов и запускает его:
my_car.start => ...roar!"
Это обычный способ реализовать множественное наследование в Ruby, так как мы можем включить в наш класс множество модулей, тем самым предоставляя ему множество дополнительных функций. Модули, используемые для этого, известны как «mixins».
Примечание: это также означает, что если мы определим метод с тем же именем в нашем классе Car, Ruby найдет и запустит его, и наш метод Module никогда не будет вызван.
Предварение
Когда мы добавляем модуль в наш класс, Ruby размещает его непосредственно под нашим классом. Означает, что во время поиска методов Ruby встретится с методами, определенными в модуле, перед методами, определенными в нашем классе. В результате любые методы модуля будут эффективно переопределять любые методы класса с тем же именем. Давайте попробуем это:
module ElectricEngine def drive; "eco-driving!"; end end class Car prepend ElectricEngine end my_car = Car.new my_car.drive => eco-driving!
Заметьте, что если мы теперь вызовем метод drive
для my_car
, мы получим реализацию метода с добавленным модулем вместо реализации класса Car
. Предшествующие модули могут быть очень полезны, когда мы хотим расширить логику нашего метода в соответствии с различными внешними условиями. Например, мы хотим измерить производительность наших методов при работе в тестовой среде. Вместо того чтобы загрязнять наши методы операторами if
и программным кодом, мы можем поместить наши специализированные методы в отдельный модуль и добавить модуль к нашему классу, только если мы находимся в тестовой среде:
module Instrumentable require 'objspace' def drive puts ObjectSpace.count_objects[:FREE] super puts ObjectSpace.count_objects[:FREE] end end class Car prepend Instrumentable if ENV['RACK_ENV'] = 'test' end my_car = Car.new.drive => 711 => driving => 678
Включение и добавление к экземплярам
Поскольку экземпляры объектов размещаются вне иерархии ПЗУ, нет «выше» или «ниже» их, где мы можем разместить модули (см. Диаграмму поиска метода). Это означает, что мы не можем включать или добавлять модули к объектам экземпляра.
Расширение классов
Когда мы расширяем наш объект класса модулем, Ruby размещает его непосредственно над собственным классом нашего объекта:
module SuperCar def fly; "flying!"; end end class Car extend SuperCar end
Наши экземпляры Car не могут получить доступ к методу fly
, так как поиск метода начнется с экземпляра car my_car
, перейдет прямо к собственному классу my_car
а затем — к классу Car
и его предкам. Метод fly
никогда не будет найден на этом пути:
my_car.fly => NoMethodError: undefined method `fly' for #<Car:0x000000019ae8d0>
Но что, если мы вместо этого вызовем метод нашего объекта класса?
Car.fly => flying
Наш объект-получатель теперь сам класс Car
. Ruby сначала посмотрит направо ( Car
), а затем до модуля SuperCar
где, и вот, он найдет метод fly
. Таким образом, расширение класса с помощью модуля эффективно дает нашему классу новые методы класса.
Расширение экземпляров
Мы можем расширить конкретный экземпляр нашего класса, например так:
my_car.extend(SuperCar)
Это поместит модуль SuperCar над собственным классом нашего объекта.
Теперь мы можем позвонить на нашем автомобиле:
my_car.fly => flying
Но не на любой другой машине:
another_car.fly => NoMethodError: undefined method `fly' for #<Car:0x000000012ae868>
Расширение объекта экземпляра с помощью модуля является отличным способом динамического декорирования конкретных экземпляров класса. Я все время использую его в приложениях Rails, чтобы легко и быстро реализовать шаблон Presenter (то есть объекты модели, которые я хочу показать в своих представлениях)
<%= my_article.extend(Presentable).title %>
эпилог
Модули гораздо больше, чем можно описать в одной статье. Понимание того, как модули вписываются в объектную модель Ruby, необходимо для их полного и творческого использования. Дайте нам знать, как модули вписываются в ваш дизайн, и обо всех интересных вещах, которые вы делаете с ними!