RubyMiddleCoding

Что такое метапрограммирование в 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.
  • eval vs. define_methodeval/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.

Sources

Related topics