Можно ли получить доступ к динамическому методу во внешнем блоке в дочернем блоке? — Ruby Meta Programming

#ruby

#ruby

Вопрос:

У меня есть DSL, который позволяет мне динамически писать код ruby.

Outer Класс принимает пользовательский блок кода для обработки.

Существует также хорошо известный метод DSL settings , который может использовать свой собственный блок кода для целей настройки.

Я хочу иметь возможность создавать повторно используемые методы в другом блоке и иметь их доступными из внутреннего блока.

При написании примера кода для этого поста я наткнулся на использование, которое работает путем присвоения self a variable во внешней области и вызова метода variable в дочерней области.

Я бы предпочел НЕ назначать self переменной, и я заметил, что если я попытаюсь сделать что-то подобное в RSPEC, то мне не нужно использовать variable = self , я могу определять методы в родительских блоках, и они доступны в дочерних блоках, см. Последний пример.

Классы

 class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(amp;block)
    instance_eval(amp;block)
  end
end

class Outer
  def initialize(amp;block)
    instance_eval(amp;block)
  end

  def build_settings(amp;block)
    Settings.new(amp;block)
  end
end
  

Запустите код

 Outer.new do

  # Create a method dynamically in the main block
  def useful_method
    '** result of the useful_method **'
  end

  x = self

  settings = build_settings do
    self.a = 'aaaa'
    self.b = useful_method()    # Throws exception
    self.c = x.useful_method()  # Works
  end

end
  

Запустите код (с подробным протоколированием)

 # Helper to colourize the console log
class String
  def error;          "33[31m#{self}33[0m" end
  def success;        "33[32m#{self}33[0m" end
end

# Run code with detailed logging
Outer.new do

  # Create a method dynamically in the main block
  def useful_method
    '** result of the useful_method **'
  end

  puts "respond?: #{respond_to?(:useful_method).to_s.success}"

  x = self

  settings = build_settings do
    puts "respond?: #{respond_to?(:useful_method).to_s.error}"
    self.a = 'aaaa'
    begin
      self.b = useful_method().success
    rescue
      self.b = 'bbbb'.error
    end
    begin
      self.c = x.useful_method().success
    rescue
      self.c = 'cccc'.error
    end
  end

  puts "a: #{settings.a}"
  puts "b: #{settings.b}"
  puts "c: #{settings.c}"

end
  

Консольный журнал из запущенного кода

  • Присвоение b вызывает исключение
  • Назначение c работает нормально

введите описание изображения здесь

Пример в RSpec, где вам не нужно назначать self

Почему я могу получить доступ к usefull_method в RSpec DSL, но не в моем собственном.

 RSpec.describe 'SomeTestSuite' do
  context 'create method in this outer block' do
    def useful_method
      'david'
    end

    it 'use outer method in this inner block' do
      expect(useful_method).to eq('david')
    end
  end
end
  

Ответ №1:

Вы могли бы передать Outer экземпляр в Settings.new :

 class Outer
  def initialize(amp;block)
    instance_eval(amp;block)
  end

  def build_settings(amp;block)
    Settings.new(self, amp;block)
    #            ^^^^
  end
end
  

и изнутри Settings использовать method_missing для делегирования вызовов неопределенных методов outer :

 class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(outer, amp;block)
    @outer = outer
    instance_eval(amp;block)
  end

  private

  def method_missing(name, *args, amp;block)
    return super unless @outer.respond_to?(name)

    @outer.public_send(name, *args, amp;block)
  end

  def respond_to_missing?(name, include_all = false)
    @outer.respond_to?(name, include_all) or super
  end
end
  

Таким образом, useful_method может быть вызван без явного получателя.

Комментарии:

1. хорошо, я попробую это завтра

Ответ №2:

Для этого вы можете использовать метод Forwardable#def_delegator .

Мы начинаем с создания классов Outer и Settings .

 class Outer
  attr_reader :settings

  def initialize(amp;block)
    instance_eval(amp;block)
  end

  def build_settings(amp;block)
    @settings = Settings.new(amp;block)
  end
end
  
 class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(amp;block)
    instance_eval(amp;block)
  end
end
  

Я включил переменную экземпляра @settings Outer для хранения экземпляра Settings , который будет создаваться динамически Outer#build_settings .

Теперь мы создаем экземпляр Outer с блоком.

 require 'forwardable'

outer = Outer.new do
  def useful_method
    '** result of the useful_method **'
  end

  Settings.extend Forwardable
  Settings.public_send(:attr_accessor, :outer)
  Settings.public_send(:def_delegator, :@outer, :useful_method)
  x = self

  settings = build_settings do
    self.outer = x
    self.a = 'aaaa'
    self.b = useful_method
  end
end
  #=> #<Outer:0x00007ffa6f9da320 @settings=#<Settings:0x00007ffa6f9d8318
  #     @a="aaaa", @outer=#<Outer:0x00007ffa6f9da320 ...>,
  #     @b="** result of the useful_method **">>
  

Как вы видите, блок выполняет следующие операции.

  • создан метод экземпляра Outer#useful_method
  • extend используется для включения Forwardable в Settings ‘ одноэлементный класс
  • средства доступа для чтения и записи для @outer создаются в Settings
  • вызовы метода экземпляра useful_method в Settings делегируются значению @outer , вызывающему Outer#useful_method вызов
  • создается экземпляр Settings , и его переменные экземпляра, @outer , @a и @b , инициализируются, причем @outer устанавливается равным экземпляру Outer только что созданного.

Теперь мы можем извлечь Settings только что созданный экземпляр и проверить значения его переменных экземпляра.

 settings = outer.settings
  #=> #<Settings:0x00007ffa6f9d8318 @a="aaaa",
  #     @outer=#<Outer:0x00007ffa6f9da320
  #     @settings=#<Settings:0x00007ffa6f9d8318 ...>>,
  #     @b="** result of the useful_method **">  
  
 settings.outer
  #=> #<Outer:0x00007ffa6f9da320 @settings=#<Settings:0x00007ffa6f9d8318
  #     @a="aaaa", @outer=#<Outer:0x00007ffa6f9da320 ...>,
  #     @b="** result of the useful_method **">>
settings.a
  #=> "aaaa" 
settings.b
  #=> "** result of the useful_method **"