Что такое метапрограммирование в Ruby? Приведите примеры с method_missing, define_method.
Метапрограммирование в Ruby — генерация кода во время выполнения. method_missing перехватывает вызовы несуществующих методов, define_method программно создаёт методы через замыкания. Основа ActiveRecord и DSL-фреймворков.
Метапрограммирование в Ruby
Метапрограммирование — написание кода, который генерирует или модифицирует другой код во время выполнения. Ruby предоставляет для этого богатый API: открытые классы, introspection, динамическое определение методов и перехват вызовов несуществующих методов. Это основа таких фреймворков, как ActiveRecord, RSpec, Grape.
method_missing
method_missing(name, *args, &block) вызывается интерпретатором, когда объект не может найти метод с данным именем ни в собственном классе, ни в предках. Переопределив его, можно перехватить любой вызов и обработать динамически.
class FlexibleQuery
def initialize(table)
@table = table
@conditions = {}
end
# Динамически обрабатывает вызовы вида find_by_FIELD(value)
def method_missing(name, *args)
if name.to_s.start_with?('find_by_')
field = name.to_s.sub('find_by_', '')
@conditions[field] = args.first
self # для чейнинга
else
super # ВАЖНО: передать вверх, если не обрабатываем
end
end
# ОБЯЗАТЕЛЬНО переопределять вместе с method_missing
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?('find_by_') || super
end
def to_sql
where_clause = @conditions.map { |k, v| "#{k} = '#{v}'" }.join(' AND ')
"SELECT * FROM #{@table} WHERE #{where_clause}"
end
end
q = FlexibleQuery.new('users')
.find_by_name('Alice')
.find_by_status('active')
puts q.to_sql
# => SELECT * FROM users WHERE name = 'Alice' AND status = 'active'
puts q.respond_to?(:find_by_email) # => true
define_method
define_method — метод модуля Module, позволяющий программно создавать методы внутри тела класса. В отличие от eval, он type-safe и захватывает локальные переменные через замыкание.
class Report
FORMATS = %i[pdf csv xlsx html]
# Генерируем методы render_pdf, render_csv, render_xlsx, render_html
FORMATS.each do |fmt|
define_method("render_#{fmt}") do |data|
"Rendering #{data.size} rows as #{fmt.upcase}"
end
end
end
r = Report.new
puts r.render_pdf([1, 2, 3]) # => Rendering 3 rows as PDF
puts r.render_csv([1, 2]) # => Rendering 2 rows as CSV
puts r.respond_to?(:render_xlsx) # => true
class_eval / module_eval
class_eval открывает контекст класса и позволяет определять методы с полным доступом к его области видимости. Полезно при добавлении методов к существующему классу из внешнего кода.
class User
attr_accessor :name, :email, :role
end
# Добавляем методы-предикаты для каждой роли
%w[admin moderator guest].each do |role|
User.class_eval do
define_method("#{role}?") do
self.role == role
end
end
end
u = User.new
u.role = 'admin'
puts u.admin? # => true
puts u.guest? # => false
instance_variable_get / set и send
class Config
def initialize
@debug = false
@timeout = 30
end
end
cfg = Config.new
# Читаем и пишем instance variables по имени
puts cfg.instance_variable_get(:@timeout) # => 30
cfg.instance_variable_set(:@timeout, 60)
puts cfg.instance_variable_get(:@timeout) # => 60
# send вызывает любой метод, включая приватные
puts cfg.send(:instance_variable_get, :@debug) # => false
Подводные камни
- Забытый
superв method_missing — если не вызватьsuperдля необрабатываемых имён, объект «проглотит» любой неверный вызов, и отладка станет кошмаром. - Отсутствие
respond_to_missing?—method_missingбез него ломаетrespond_to?,method(:name), сериализацию. Всегда переопределяйте оба метода вместе. - Производительность —
method_missingмедленнее обычного dispatch. Если метод вызывается часто, кешируйте результат черезdefine_methodпри первом вызове (паттерн «ghost method»). - Бесконечная рекурсия — опечатка в имени метода внутри
method_missing(например,@conditions[feild]) снова триггеритmethod_missing, что ведёт кSystemStackError. - Читаемость и поддержка — динамически сгенерированные методы не видны в IDE, не индексируются grep/ctags, и их нельзя найти через
instance_methodsбез форсированного вызова. Документируйте через YARD@!method. evalvs.define_method—eval/class_evalсо строковым аргументом и интерполяцией пользовательских данных — уязвимость инъекции кода. Предпочитайтеdefine_methodс блоком.- Сложность стека вызовов — метапрограммирование размывает трассировку ошибок. Добавляйте явные сообщения в
raiseс контекстом метода.
Common mistakes
- Сводить metaprogramming к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: объектная динамическая модель Ruby, где почти всё является объектом, а методы ищутся через ancestor chain.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет metaprogramming через конкретную точку lifecycle в Ruby.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.