Модули и миксины

Наследование позволяет иметь только одного родителя. Когда нужно подмешать поведение из нескольких источников, на сцену выходят модули — главный механизм переиспользования кода в Ruby.
Суть: модуль — это контейнер методов, который нельзя инстанцировать; include подмешивает его методы в класс (миксин), решая проблему одиночного наследования; модули также служат пространствами имён.

Модуль решает две задачи. Первая — группировать связанные методы и константы под общим именем (пространство имён). Вторая, главная — «миксин»: подмешать набор методов сразу в несколько разных классов, не связывая их наследованием. Так разделяемое поведение (например, «умею сравниваться») переиспользуется без дублирования.

module Walkable
  def walk
    "#{name} идёт пешком"
  end
end

module Swimmable
  def swim
    "#{name} плывёт"
  end
end

class Duck
  include Walkable    # подмешиваем оба
  include Swimmable
  attr_reader :name
  def initialize(name) = @name = name
end

d = Duck.new("Утя")
puts d.walk   # => Утя идёт пешком
puts d.swim   # => Утя плывёт

Разбор: include, prepend, extend

Три способа подмешать модуль различаются местом в цепочке предков. include ставит модуль ниже класса (методы класса побеждают). prepend ставит выше (методы модуля побеждают, удобно для «обёрток»). extend добавляет методы модуля как методы класса, а не экземпляра.

class Temperature
  include Comparable          # подмешиваем сравнения
  attr_reader :degrees
  def initialize(d) = @degrees = d
  def <=>(other)              # реализуем ОДИН метод
    degrees <=> other.degrees
  end
end

a = Temperature.new(20)
b = Temperature.new(30)
puts a < b              # => true
puts [b, a].min.degrees # => 20
# <, >, ==, between?, clamp — всё из Comparable бесплатно!

Как работает под капотом

Модуль вставляется в цепочку предков класса. Реализовав один метод <=> и включив Comparable, вы получаете <, >, ==, between? и другие — они написаны внутри Comparable через ваш <=>. Аналогично Enumerable строит map/select на вашем each. Это и есть сила миксинов: чуть-чуть реализуете — много получаете.

   include против prepend в цепочке предков:

   include Walkable           prepend Logger
   --------------------       --------------------
   [ Duck     ]               [ Logger   ]  <- ВЫШЕ класса
   [ Walkable ]  <- НИЖЕ      [ Duck     ]
   [ Swimmable]               [ Object   ]
   [ Object   ]

   include: метод класса важнее модуля
   prepend: метод модуля перехватывает вызов (обёртка)

Та же идея «подмешать общее поведение» на Python — это множественное наследование и миксин-классы:

# Та же логика на Python ▶
from functools import total_ordering

@total_ordering              # аналог Comparable
class Temperature:
    def __init__(self, d):
        self.degrees = d
    def __eq__(self, o): return self.degrees == o.degrees
    def __lt__(self, o): return self.degrees < o.degrees

print(Temperature(20) < Temperature(30))   # True

Частые ошибки

  • Пытаться создать объект модуля. Walkable.new — ошибка: модуль нельзя инстанцировать, его только подмешивают.
  • Путать include и extend. include даёт методы экземпляра, extend — методы класса. Перепутали — метод не там, где ждёте.
  • Конфликт имён. Если два модуля определяют одинаковый метод, побеждает включённый позже — это легко упустить.

Best practices

  • Используйте модули для разделяемого поведения вместо глубокого наследования — это гибче.
  • Подключайте Comparable (реализовав <=>) и Enumerable (реализовав each) к своим классам — получите массу методов даром.
  • Применяйте модули как пространства имён, чтобы избежать конфликтов имён классов в больших проектах.

Глубже: модули как пространства имён

Мы подробно разобрали миксины, но у модулей есть вторая, не менее важная роль — организация кода в пространства имён. В большом проекте легко получить конфликт: два разных гема определяют класс с одинаковым именем Client, и они затирают друг друга. Модуль решает это, оборачивая классы: Stripe::Client и Twilio::Client мирно сосуществуют, потому что живут в разных пространствах. Двойное двоеточие :: — это «загляни внутрь модуля». Пространства имён группируют связанный код в логические единицы, делают зависимости явными и предотвращают засорение глобального пространства. В Rails вы постоянно будете видеть такую вложенность: ActiveRecord::Base, ActionController::Base. Модули также удобны как контейнеры для констант и «утилитных» методов, не привязанных к конкретному объекту — например, Math::PI и Math.sqrt. Таким образом модуль в Ruby — это двуликий инструмент: с одной стороны, миксин, подмешивающий поведение в классы, с другой — пространство имён, организующее код. Обе роли вы будете использовать постоянно, и обе делают модули, пожалуй, самой характерной чертой архитектуры Ruby.

Итог. Модули — контейнеры методов, подмешиваемые в классы через include/prepend/extend. Они решают проблему одиночного наследования и лежат в основе Comparable и Enumerable, где одна реализация (<=> или each) даёт десятки готовых методов.

Проверьте себя
1. Зачем включать модуль Comparable в свой класс?
AЧтобы класс стал быстрее
BРеализовав один метод <=>, бесплатно получить <, >, ==, between? и другие сравнения
CЧтобы запретить наследование
DЧтобы класс нельзя было создать
2. Чем include отличается от extend при подмешивании модуля?
Ainclude добавляет методы экземпляра, а extend — методы класса
BНичем
Cextend работает быстрее
Dinclude нельзя использовать дважды