47

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.

4
  • 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. Commented Jul 6, 2016 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? Commented Jul 6, 2016 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. Commented Jul 6, 2016 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. Commented Jul 6, 2016 at 20:46

3 Answers 3

59

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
Sign up to request clarification or add additional context in comments.

5 Comments

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?
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.
Thank you very much! I'll look into it more to be more comfortable with Metaprogramming but for now, you helped me a lot! :)
The more idiomatic way would be to use define_singleton_method.
The distinction of defining a class method vs an instance method is extremely important for the sake of performance. Thanks for mentioning that @Jordan
12

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

1 Comment

This keeps giving me a NoMethodError :/
4

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>)

3 Comments

If this is rails than this is a useless exercise since find_by_XXX is already defined. Also define_method can accept a String without issue.
method_missing is something I hate. There is no easy way to find methods defined in this way. Avoid if possible.
@akostadinov agreed

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.