J'ai un DSL qui me permet d'écrire du code ruby ​​de manière dynamique.

La classe Outer prend un bloc de code personnalisé à traiter.

Il existe également une méthode DSL bien connue appelée settings qui peut prendre son propre bloc de code à des fins de configuration.

Je veux pouvoir créer des méthodes réutilisables dans l'autre bloc et les avoir disponibles à partir du bloc interne.

En écrivant l'exemple de code pour cet article, je suis tombé sur une utilisation qui fonctionne en attribuant self à un variable dans la portée externe et en appelant la méthode sur le variable dans la portée enfant.

Je préférerais ne PAS avoir besoin de m'attribuer à une variable et j'ai remarqué que si j'essayais de faire quelque chose de similaire dans RSPEC, alors je n'ai pas besoin d'utiliser variable = self, je peux définir des méthodes dans les blocs parents et ils sont disponibles dans les blocs enfants, voir le dernier exemple.

Des classes

class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(&block)
    instance_eval(&block)
  end
end

class Outer
  def initialize(&block)
    instance_eval(&block)
  end

  def build_settings(&block)
    Settings.new(&block)
  end
end

Exécutez le code

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

Exécutez le code (avec une journalisation détaillée)

# Helper to colourize the console log
class String
  def error;          "\033[31m#{self}\033[0m" end
  def success;        "\033[32m#{self}\033[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

Journal de la console à partir du code en cours d'exécution

  • L'attribution à b lève une exception
  • L'attribution à c fonctionne bien

enter image description here

Exemple dans RSpec où vous n'avez pas besoin d'attribuer self

Pourquoi puis-je accéder à usefull_method dans le RSpec DSL, mais pas dans le mien.

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
3
David Cruwys 26 oct. 2020 à 14:52

2 réponses

Meilleure réponse

Vous pouvez transmettre l'instance Outer à Settings.new:

class Outer
  def initialize(&block)
    instance_eval(&block)
  end

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

Et depuis Settings utilisez method_missing pour déléguer des appels de méthode non définis à outer:

class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(outer, &block)
    @outer = outer
    instance_eval(&block)
  end

  private

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

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

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

De cette façon, useful_method peut être appelé sans récepteur explicite.

3
Stefan 26 oct. 2020 à 12:22

Vous pouvez utiliser la méthode Forwardable #def_delegator pour cela.

Nous commençons par créer les classes Outer et Settings.

class Outer
  attr_reader :settings

  def initialize(&block)
    instance_eval(&block)
  end

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

  def initialize(&block)
    instance_eval(&block)
  end
end

J'ai inclus une variable d'instance @settings dans Outer pour contenir une instance de Settings qui sera créée dynamiquement par Outer#build_settings.

Nous créons maintenant une instance de Outer avec un bloc.

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

Comme vous le voyez, les opérations suivantes sont effectuées par le bloc.

  • la méthode d'instance Outer#useful_method est créée
  • extend est utilisé pour inclure Forwardable dans la classe singleton de Settings
  • Les accesseurs de lecture et d'écriture pour @outer sont créés dans Settings
  • les appels à la méthode d'instance useful_method dans Settings sont délégués à la valeur de @outer, provoquant l'appel de Outer#useful_method
  • une instance de Settings est créée et ses variables d'instance, @outer, @a et @b, sont initialisées, @outer étant égal à l'instance de { {X5}} vient d'être créé.

Nous pouvons maintenant récupérer l'instance de Settings qui vient d'être créée et examiner les valeurs de ses variables d'instance.

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 **" 
0
Cary Swoveland 27 oct. 2020 à 04:05