Reputation: 1171
I'm working on a fairly simple site that allows users to choose recipe ingredients, their quantities and then shows them nutritional info based on their recipe and a large database.
Right now, I feel like I'm repeating myself a bit. I want to be able to make this "DRY" by having one method each in the Recipe and Recipe_Ingredient model that will do the same thing only accept the right parameter, which will be the type of nutrient.
Here is the relevant code in my view that currently calls two different methods (and will call more when extended to the other nutrients):
<ul>Calories <%= @recipe.total_calories %></ul>
<ul>Fat (grams) <%= @recipe.total_fat %></ul>
In my recipe model, I have methods that iterate over each of the ingredients in the recipe:
def total_calories
recipe_ingredients.to_a.sum { |i| i.total_calories }
end
def total_fat
recipe_ingredients.to_a.sum { |i| i.total_fat }
end
In the block, we call two separate methods that actually calculate the nutrients for each individual recipe ingredient:
def total_calories
ingredient.calories*ingredient.weight1*quantity/100
end
def total_fat
ingredient.fat*ingredient.weight1*quantity/100
end
This last piece is where we reference the database of ingredients. For context, here are the relationships:
class RecipeIngredient < ActiveRecord::Base
belongs_to :ingredient
belongs_to :recipe
class Recipe < ActiveRecord::Base
has_many :recipe_ingredients
Thanks in advance for any help.
Upvotes: 0
Views: 407
Reputation: 619
You could use meta programming to dynamically add the methods. Here is a start, you can get even more DRY than this.
class DynamicTotalMatch
attr_accessor :attribute
def initialize(method_sym)
if method_sym.to_s =~ /^total_of_(.*)$/
@attribute = $1.to_sym
end
end
def match?
@attribute != nil
end
end
Recipe
class Recipe
def self.method_missing(method_sym, *arguments, &block)
match = DynamicTotalMatch.new(method_sym)
if match.match?
define_dynamic_total(method_sym, match.attribute)
send(method_sym, arguments.first)
else
super
end
end
def self.respond_to?(method_sym, include_private = false)
if DynamicTotalMatch.new(method_sym).match?
true
else
super
end
end
protected
def self.define_dynamic_total(method, attribute)
class_eval <<-RUBY
def self.#{method}(#{attribute})
recipe_ingredients.to_a.sum { |i| i.send(attribute)
end
RUBY
end
end
RecipeIngredient
class RecipeIngredient
def self.method_missing(method_sym, *arguments, &block)
match = DynamicTotalMatch.new(method_sym)
if match.match?
define_dynamic_total(method_sym, match.attribute)
send(method_sym, arguments.first)
else
super
end
end
def self.respond_to?(method_sym, include_private = false)
if DynamicTotalMatch.new(method_sym).match?
true
else
super
end
end
protected
def self.define_dynamic_total(method, attribute)
class_eval <<-RUBY
def self.#{method}(#{attribute})
ingredient.send(attribute) * ingredient.weight1 * quantity / 100
end
RUBY
end
end
Example was copied from ActiveRecord and this page: http://technicalpickles.com/posts/using-method_missing-and-respond_to-to-create-dynamic-methods/
Upvotes: 0
Reputation: 140
The send method with a symbol parameter works well for that kind of DRY.
<ul>Calories <%= @recipe.total :calories %></ul>
<ul>Fat (grams) <%= @recipe.total :fat %></ul>
Recipe
def total(type)
recipe_ingredients.to_a.sum { |i| i.total type }
end
RecipeIngredient
def total(type)
ingredient.send(type) * ingredient.weight1 * quantity / 100
end
Upvotes: 0