Supongamos que queremos crear una Clase Recipe
cuyo constructor entienda
un Lenguaje de Dominio Específico (Domain Specific Language, DSL) para describir recetas
de cocina.
Queremos que sea posible especificar con facilidad los ingredientes y los
pasos de la forma mas simple posible.
La API debería ser parecida a esta:
puts Recipe.new("Noodles and Cheese") { ingredient "Water", amount => "2 cups" ingredient "Noodles", amount => "1 cup" ingredient "Cheese", amount => "1/2 cup" step "Heat water to boiling.", during => "5 minutes" step "Add noodles to boiling water.", during => "6 minutes" step "Drain water." step "Mix cheese in with noodles." }
y el resultado de ejecutar el código anterior debería ser similar a este:
~/Chapter8ReflectionandMetaprogramming$ ruby recipes.rb Noodles and Cheese ================== Ingredients: Water (2 cups), Noodles (1 cup), Cheese (1/2 cup) 1) Heat water to boiling. (5 minutes) 2) Add noodles to boiling water. (6 minutes) 3) Drain water. 4) Mix cheese in with noodles.
Como se ve en el ejemplo de uso, el constructor de la clase recibe un bloque que describe la receta.
Una solución es usar yield
para llamar al bloque:
~/Chapter8ReflectionandMetaprogramming$ cat -n recipes2.rb 1 class Recipe2 2 attr_accessor :name, :ingredients, :instructions 3 4 def initialize(name) 5 self.name = name 6 self.ingredients = [] 7 self.instructions = [] 8 9 yield self 10 end 11 12 def to_s 13 output = name 14 output << "\n#{'=' * name.size}\n\n" 15 output << "Ingredients: #{ingredients.join(', ')}\n\n" 16 17 instructions.each_with_index do |instruction, index| 18 output << "#{index + 1}) #{instruction}\n" 19 end 20 21 output 22 end 23 24 def ingredient(name, options = {}) 25 ingredient = name 26 ingredient << " (#{options[:amount]})" if options[:amount] 27 28 ingredients << ingredient 29 end 30 31 def step(text, options = {}) 32 instruction = text 33 instruction << " (#{options[:during]})" if options[:during] 34 35 instructions << instruction 36 end 37 endLos métodos
ingredient
y step
nos ayudan en el proceso de construcción de la cadena
de presentación.
Sin embargo, esta solución no es satisfactoria, ya que debemos pasar al bloque de definición el objeto receta que esta siendo construido:
39 noodles_and_cheese = Recipe2.new("Noodles and Cheese") do |r| 40 r.ingredient "Water", :amount => "2 cups" 41 r.ingredient "Noodles", :amount => "1 cup" 42 r.ingredient "Cheese", :amount => "1/2 cup" 43 44 r.step "Heat water to boiling.", :during => "5 minutes" 45 r.step "Add noodles to boiling water.", :during => "6 minutes" 46 r.step "Drain water." 47 r.step "Mix cheese in with noodles." 48 end 49 50 puts noodles_and_cheesePeor aún, debemos repetir constantemente el objeto
r.ingredient
y r.step
.
Sabemos que instance_eval
evalúa el bloque que se le pasa en el contexto del objeto
en el que es llamado (self
en la línea 9 abajo):
~/Chapter8ReflectionandMetaprogramming$ cat -n recipes.rb 1 class Recipe 2 attr_accessor :name, :ingredients, :instructions 3 4 def initialize(name, &block) 5 self.name = name 6 self.ingredients = [] 7 self.instructions = [] 8 9 instance_eval &block 10 end 11 12 def to_s 13 output = <<"EORECIPE" 14 #{name} 15 #{'=' * name.size} 16 17 Ingredients: #{ingredients.join(', ')} 18 19 #{ 20 out = "" 21 instructions.each_with_index do |instruction, index| 22 out << "#{index + 1}) #{instruction}\n" 23 end 24 out 25 } 26 EORECIPE 27 end 28 29 def ingredient(name, options = {}) 30 ingredient = name 31 ingredient << " (#{options[:amount]})" if options[:amount] 32 33 ingredients << ingredient 34 end 35 36 def step(text, options = {}) 37 instruction = text 38 instruction << " (#{options[:for]})" if options[:for] 39 40 instructions << instruction 41 end 42 43 def amount 44 :amount 45 end 46 47 def during 48 :for 49 end 50 end 51 52 puts Recipe.new("Noodles and Cheese") { 53 ingredient "Water", amount => "2 cups" 54 ingredient "Noodles", amount => "1 cup" 55 ingredient "Cheese", amount => "1/2 cup" 56 57 step "Heat water to boiling.", during => "5 minutes" 58 step "Add noodles to boiling water.", during => "6 minutes" 59 step "Drain water." 60 step "Mix cheese in with noodles." 61 }
Casiano Rodriguez León 2015-01-07