Наследование в Ruby — это не просто class Child < Parent. Это целый арсенал методов для тонкой настройки поведения классов. В статье разберём ключевые инструменты работы с иерархией: от базового superclass до магии prepended модулей, и покажем, как избежать “цепочки зомби” в сложных проектах.
Вы когда-нибудь видели код, где ancestors возвращает массив длиннее списка покупок на неделю?
Пора разобраться, кто здесь главный в иерархии наследования.
🧠 Теория: как Ruby ищет методы?
Когда вы вызываете object.method, Ruby:
- Смотрит в класс объекта
- Идёт по цепочке модулей (если есть
prependилиinclude) - Проверяет родительский класс
- Повторяет шаги 1-3 для суперклассов
- Взывает к
method_missing
Именно эту цепочку мы и будем исследовать.
🔧 Ключевые методы
1. superclass — родитель в очевидном месте
class Animal; end
class Cat < Animal; end
Cat.superclass #=> Animal
Animal.superclass #=> Object
Object.superclass #=> BasicObject
BasicObject.superclass #=> nil
Когда использовать:
- Проверка прямого наследования (“является ли Cat подклассом Animal?”)
- Построение простых иерархий (например, разные типы платежей)
Антипаттерн:
# Плохо: проверка через superclass.superclass.superclass
def mammal?(animal)
animal.superclass.superclass == Mammal
end
2. ancestors — полная родословная класса
module M; end
class A; end
class B < A
include M
end
B.ancestors #=> [B, M, A, Object, Kernel, BasicObject]
Жизненный пример:
Допустим, у вас есть система ролей:
module AdminPermissions; end
module UserPermissions; end
class User; end
class Admin < User
include AdminPermissions
end
Admin.ancestors #=> [Admin, AdminPermissions, User, Object...]
Когда использовать:
- Отладка “почему вызвался не тот метод?”
- Динамическое определение возможностей объекта
3. included — модуль как микс-ин
module Loggable
def self.included(base)
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
end
module ClassMethods
def log_level; :info; end
end
module InstanceMethods
def log(msg); puts "[#{self.class.log_level}] #{msg}"; end
end
end
class Product
include Loggable
end
Product.log_level #=> :info
Product.new.log("Created") #=> "[info] Created"
Паттерн:
Используйте для добавления сразу и классовых, и инстанс-методов.
Боль:
Если забыть base.extend, классовые методы не появятся.
4. extended — модуль как статический хелпер
module Pagination
def self.extended(base)
base.class_eval do
scope :per_page, ->(n) { limit(n) }
end
end
def total_pages; (count / 10.0).ceil; end
end
class Post < ActiveRecord::Base
extend Pagination
end
Post.total_pages #=> 3
Post.per_page(5) #=> AR scope
Контраст с included:
include— добавляет методы в экземплярыextend— добавляет методы в сам класс
5. prepended — модуль, который “перебивает” методы
module SoftDelete
def self.prepended(base)
base.scope :active, -> { where(deleted_at: nil) }
end
def destroy
update(deleted_at: Time.current)
end
end
class Comment < ActiveRecord::Base
prepend SoftDelete
end
comment = Comment.create!
comment.destroy # Вызовет метод из SoftDelete, а не из AR
Когда использовать:
- Переопределение методов без monkey-patching
- Приоритетное выполнение кода (например, before-фильтры)
Опасность:
Если в модуле не вызвать super, цепочка вызовов прервётся.
💥 Антипаттерны
- Спагетти-наследование
class A < B; include M; prepend N; extend P; end # Теперь попробуйте представить ancestors... -
Злоупотребление prepend
Переопределение базовых методов в 10 модулях — верный способ получить неотлаживаемый код. - Модули-швейцары
Когда один модуль иinclude, иextend, иprepend— это нарушение SRP.
🎤 Что сказать на собеседовании
— Как вы организуете сложную иерархию классов?
— Использую модули для горизонтального расширения функциональности (include/extend), а наследование — только для выражения отношения “является”. Для переопределения методов предпочитаю prepend перед monkey-patching.
🧾 Вывод
Цепочка наследования в Ruby — как матрёшка:
superclassпоказывает прямого родителяancestorsраскрывает всю цепочкуincluded/extended/prepended— крючки для модулей
Правильное использование этих инструментов превращает запутанную иерархию в прозрачную архитектуру. Главное — не превращать код в “дом, который построил Джек” из 20 уровней наследования.