Ruby Moment of Zen: #method_missing

Derek
Derek
04
Jul

Realizing that hitting “talk” on my phone dialed the previous number. Figuring out that the arrow next to my fuel gauge showed which side the car fuel door is on. Tab completion. A couple of times a month, I’ll have a “wow – that’s so useful and so simple” moment. One of the first times I experienced that with Ruby was with #method_missing.

Every class in Ruby has a #method_missing method. When you attempt to call a method that doesn’t exist, #method_missing will throw a NoMethodError exception.

You can override this method and do some amazing things with it. At Highgroove Studios, we use #method_missing like a trophy wife uses her rich husband’s credit card bill: without abandon.

For example, let’s say you have a User model, and that user has a ton of profile data. You want to abstract that data, so you make a Profile class.


class User < ActiveRecord::Base
  has_one :profile
end

class Profile < ActiveRecord::Base
  belongs_to :user
end

However, you don’t want to call user.profile.street_address every time you need to access an attribute in a user’s profile. You also don’t want to define a bunch of reader methods like this:


def street_address
 profile.street_address
end

Here’s all you have to do:


class User < ActiveRecord::Base
  ...
  # Attempts to pass any missing methods to
  # the associated +profile+.
  def method_missing(meth, *args, &block)
    if profile.respond_to?(meth)
      profile.send(meth)
    else
      super
    end
  end
end

So, we now have this:


@user.street_address == @user.profile.street_address

That’s only one example of the power and simplicity of #method_missing.

Comments

  1. josh said 40 minutes later:

    Clever, but that’s sort of overkill for a simple delegation. You could just use the #delegate method to do the same thing without the method_missing mumbo-jumbo. Look in active_support/core_ext/module/delegation.rb to see how that works.

    While method_missing can be a powerful tool, it can also get you in a lot of trouble when overused. Just saying.

  2. Lourens said about 3 hours later:

    Hi,

    Nice writeup, but I second Josh. Delegation is much cleaner in 1-to-1 relationships.

    class Subscription < ActiveRecord::Base self.abstract_class = true belongs_to :account belongs_to :plan end

    delegate :price, :charge_this_month, :to =&gt; :plan
  3. Lourens said about 3 hours later:

    Mm, got to love Typo, the delegation line before the closing end

  4. Derek said about 19 hours later:

    You guys are right – I also used too simple of an example for this.

    However, I’d say #delegate also qualifies as a Zen moment!

  5. JEG2 said 4 days later:

    Allow me to offer up a different example from the HighGroove code vault.

    In one project we have a favorites system. It works using polymorphic associations, so there are many different kinds of “favorable” objects. The User class has an association with favorites, of course, but nine times out of ten we really want an Array of one type of favorable objects.

    For example, we might want to provide a listing of Event objects. We don’t care that Comments, FileRecords, and whatever else are favored, we want just want an Array of Events the User favors. You could add accessors for each type of favored object, but you would need to maintain that list as more or less object become favored and if the favorites system changes, you’ve got a lot of methods to update.

    Instead we use method_missing() on the User:

    <pre><code> # # This provides #favorite_class_name style methods that return a list of the # favorites of that class type: # # current_user.favorite_file_records #=> [FileRecord, FileRecord, ...] # def method_missing(meth, *args, &block) if meth.to_s =~ /^favorite_(\w+)$/ fave_type = $1.singularize.camelize.constantize favorites.map { |fave| fave.favorable }. select { |fave| fave.is_a? fave_type } else super end end </code></pre>

    With that, we can add or subtract favorable objects at will and everything just works. We also have a single point to manage changes.

Comments are disabled