Модули и миксины
Наследование позволяет иметь только одного родителя. Когда нужно подмешать поведение из нескольких источников, на сцену выходят модули — главный механизм переиспользования кода в 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) даёт десятки готовых методов.