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 end
Los 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_cheese
Peor 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