Thursday, February 28, 2008

Ruby: Replace method_missing with dynamic method definitions

You have methods you want to handle dynamically without the pain of debugging method_missing.

class Decorator
def initialize(subject)
@subject = subject
end

def method_missing(sym, *args, &block)
@subject.send sym, *args, &block
end
end

becomes

class Decorator
def initialize(subject)
subject.public_methods(false).each do |meth|
(class << self; self; end).class_eval do
define_method meth do |*args|
subject.send meth, *args
end
end
end
end
end

Motivation

Debugging classes that use method_missing can often be painful. At best you often get a NoMethodError on an object that you didn't expect, and at worst you get stack level too deep (SystemStackError).

There are times that method_missing is required. If the usage of an object is unknown, but must support unexpected method calls you may not be able to avoid the use of method_missing.

However, often you know how an object will be used and using Dynamically Define Method you can achieve the same behavior without relying on method_missing.

Mechanics
  • Dynamically define the necessary methods
  • Remove method_missing
  • Test
Example: Dynamic delegation without method_missing

Delegation is a common task while developing software. Delegation can be handled explicitly by defining methods yourself or by utilizing something from the Ruby Standard Library such as Forwardable. Using these techniques gives you control over what methods you want to delegate to the subject object; however, sometimes you want to delegate all methods without specifying them. Ruby's Standard Library also provides this capability with the delegate library, but we'll assume we need to implement our own for this example.

The simple way to handle delegation (ignoring the fact that you would want to undefine all the standard methods a class gets by default) is to use method_missing to pass any method calls straight to the subject.

class Decorator
def initialize(subject)
@subject = subject
end

def method_missing(sym, *args, &block)
@subject.send sym, *args, &block
end
end

This solution does work, but it can be problematic when mistakes are made. For example, calling a method that does not exist on the subject will result in the subject raising a NoMethodError. Since the method call is being called on the decorator, but the subject is raising the error it may be painful to track down where the problem resides.

The wrong object raising a NoMethodError is significantly better than the dreaded stack level too deep (SystemStackError). This can be caused by something as simple as forgetting to use the subject instance variable and trying to use a non-existent subject method or any misspelled method. When this happens the only feedback you have is that something went wrong, but Ruby isn't sure exactly what it was.

These problems can be avoided entirely by using the available data to dynamically define methods at run time. The following example defines an instance method on the decorator for each public method of the subject.

class Decorator
def initialize(subject)
subject.public_methods(false).each do |meth|
(class << self; self; end).class_eval do
define_method meth do |*args|
subject.send meth, *args
end
end
end
end
end

Using this technique any invalid method calls will be correctly reported as NoMethodErrors on the decorator. Additionally, there's no method_missing definition, which should help avoid the stack level too deep problem entirely.

Example: Using user defined data to define methods

Often you can use the information from a class definition to define methods instead of relying on method_missing. For example, the following code relies on method_missing to determine if any of the attributes are nil.

class Person
attr_accessor :name, :age

def method_missing(sym, *args, &block)
empty?(sym.to_s.sub(/^empty_/,"").chomp("?"))
end

def empty?(sym)
self.send(sym).nil?
end
end

The code works, but it suffers from the same debugging issues that the previous example does. Utilizing Dynamically Define Method the issue can be avoided by defining the attributes and creating the empty_attribute? methods at the same time.

class Person
def self.attrs_with_empty_methods(*args)
attr_accessor *args

args.each do |attribute|
define_method "empty_#{attribute}?" do
self.send(attribute).nil?
end
end
end

attrs_with_empty_methods :name, :age
end

13 comments:

  1. how about dynamically defining the methods when method missing is triggered?

    ex:

    class Proxy
    def method_missing(meth, *args, &block)
    if @target.respond_to?(meth)
    self.class.class_eval <<-end_eval
    def #{meth}(*args, &block)
    @target.__send__(:#{meth}, *args, &block)
    end
    end_eval
    __send__(meth, *args, &block)
    else
    super # NoMethodError
    end
    end
    end

    this way, not all the instance methods are defined but only those that are actually used.

    ReplyDelete
  2. yuck, it stripped my spaces. but I think you get my idea.

    ReplyDelete
  3. Jorrel,

    I like the example, but since it still involves using method_missing I'd avoid it if possible.

    But, it's a great alternative worth knowing about.

    Cheers, Jay

    ReplyDelete
  4. Jay, if you are delegating all of the methods to subject, here is a simpler way:

    class Decorator
    extend Forwardable

    def initialize(subject)
    @subject = subject
    (class << self; self; end).class_eval do
    def_delegators :'@subject', *subject.public_methods(false)
    end
    end
    end

    - Paul and Ali

    ReplyDelete
  5. Great example. I modified it to suit what i needed, and just now looking at the comments i realized i came up with something very similar to Jorrell... http://www.rubyrescue.com/?p=11

    ReplyDelete
  6. Daniel9:15 PM

    How would you apply this when the initialize method is never called but allocate is used instead?

    Thanks, Daniel

    ReplyDelete
  7. You could redefine allocate, I imagine. Other than that, nothing comes to mind.

    Cheers, Jay

    ReplyDelete
  8. If you REALLY want to delegate all methods to the subject, why not just use the Delegator class from the standard library, which does it for you very easily?

    http://ruby-doc.org/stdlib/libdoc/delegate/rdoc/index.html

    ReplyDelete
  9. Thanks Jay,

    this was super helpful. Because association_proxy calls send, it won't call method_missing on the target object. Thus any of your dynamic methods don't exist when you use them via the association.

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. Don't forget the block part:

    class Decorator
      def initialize(subject)
        subject.public_methods(false).each do |meth|
          (class << self; self; end).class_eval do
            define_method meth do |*args, &block|
              subject.send meth, *args, &block
            end
          end
        end
      end
    end

    ReplyDelete
  12. I ran into an issue with this methodology. If you Marshal the object when you reload the object the dynamically mapped methods no longer exist. You can "fix" this by creating a new instance of the class but that's not something downstream code should have to do.

    Ironically I'm trying to see if I can fix it by intercepting method missing.

    ReplyDelete
  13. Older article but still relevant. Thank you :)

    ReplyDelete

Note: Only a member of this blog may post a comment.