Lev
Lev

Reputation: 1171

Ruby on Rails - Simplifying similar methods that access different variables

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

Answers (2)

christiangeek
christiangeek

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

Huston
Huston

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

Related Questions