#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 **"