41

I want to create a bunch of methods for a find_by feature. I don't want to write the same thing over and over again so I want to use metaprogramming.

Say I want to create a method for finding by name, accepting the name as an argument. How would I do it? I've used define_method in the past but I didn't have any arguments for the method to take. Here's my (bad) approach

["name", "brand"].each do |attribute|
    define_method("self.find_by_#{attribute}") do |attr_|
      all.each do |prod|
        return prod if prod.attr_ == attr_
      end
    end
  end

Any thoughts? Thanks in advance.

Kostas Andrianos
  • 1,551
  • 2
  • 16
  • 21
  • Be aware if all is a large data set this could cause performance issues. Also I sincerely hope this is outside of the rails context as rails already implements `find_by_XXX` for every attribute. – engineersmnky Jul 06 '16 at 20:32
  • 1
    Note: this will define two methods named `self.find_by_name` and `self.find_by_brand`. While it is possible to create such methods, it is impossible to call them using normal method calling syntax, because `.` is not a legal character in an identifier. Is there any particular reason why you want to define a method with an illegal name? – Jörg W Mittag Jul 06 '16 at 20:44
  • @engineersmnky It's not rails! all just returns an array of `Products` for the inventory system of a Toy store. It's for the final project for the Ruby Nanodegree at Udacity. – Kostas Andrianos Jul 06 '16 at 20:44
  • @JörgWMittag I had no idea about that, and I was wondering why the NoMethodError, since I had a self.something method. – Kostas Andrianos Jul 06 '16 at 20:46

3 Answers3

56

If I understand your question correctly, you want something like this:

class Product
  class << self
    [:name, :brand].each do |attribute|
      define_method :"find_by_#{attribute}" do |value|
        all.find {|prod| prod.public_send(attribute) == value }
      end
    end
  end
end

(I'm assuming that the all method returns an Enumerable.)

The above is more-or-less equivalent to defining two class methods like this:

class Product
  def self.find_by_name(value)
    all.find {|prod| prod.name == value }
  end

  def self.find_by_brand(value)
    all.find {|prod| prod.brand == value }
  end
end
engineersmnky
  • 25,495
  • 2
  • 36
  • 52
Jordan Running
  • 102,619
  • 17
  • 182
  • 182
  • This works for me but... Can you explain how you got here? I searched for `class << self` and saw that it opens up a class and changes it's behavior but, why would I need to do that inside the class itself? – Kostas Andrianos Jul 06 '16 at 20:39
  • 1
    You need to do that because you want to define a class method. Without that, `define_method` will define an instance method, and `define_method("self.foo")` as in your example doesn't work. – Jordan Running Jul 06 '16 at 20:40
  • Thank you very much! I'll look into it more to be more comfortable with Metaprogramming but for now, you helped me a lot! :) – Kostas Andrianos Jul 06 '16 at 20:42
  • 2
    The more idiomatic way would be to use `define_singleton_method`. – Jörg W Mittag Jul 06 '16 at 20:47
  • The distinction of defining a class method vs an instance method is extremely important for the sake of performance. Thanks for mentioning that @Jordan – frenchmd Mar 24 '17 at 18:12
11

It if you read the examples here http://apidock.com/ruby/Module/define_method you will find this one:

define_method(:my_method) do |foo, bar| # or even |*args|
  # do something
end

is the same as

def my_method(foo, bar)
   # do something
end
Albin
  • 2,912
  • 1
  • 21
  • 31
2

When you do this: define_method("self.find_by_#{attribute}")

that is incorrect. The argument to define_method is a symbol with a single word.

Let me show you some correct code, hopefully this will be clear:

class MyClass < ActiveRecord::Base
  ["name", "brand"].each do |attribute|
    define_method(:"find_by_#{attribute}") do |attr_|
      first(attribute.to_sym => attr_)
    end
  end
end

This will produce class methods for find_by_brand and find_by_name.

Note that if you're looking into metaprogramming, this is a good use-case for method_missing. here's a tutorial to use method_missing to implement the same functionality you're going for (find_by_<x>)

Drenmi
  • 8,492
  • 4
  • 42
  • 51
max pleaner
  • 26,189
  • 9
  • 66
  • 118