Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing ruby method context / calling a method with instance_exec

First, for the short version:

Isn't a method definition just a block? Why can't I do something like:

obj.instance_exec(&other_obj.method(:my_method))

with the goal of running some module method in the context of an instance of a separate class? The method is called, but it doesn't seem to be executed in the context of 'obj', despite the 'instance_exec' call.

The only way I can figure out how to accomplish this is to wrap all of the code of 'my_method' in a proc, then call in the following manner instead:

obj.instance_eval(&other_obj.my_method)

but I'd like to avoid encapsulating all of my module methods in procs.


Now, for the long version:

I'm attempting to create a modularized external provider system, where for any given class/method (generally controller methods,) I can call a corresponding method for a given provider (e.g. facebook).

Since there could be multiple providers, the provider methods need to be namespaced, but instead of simply including a bunch of methods like, for example, 'facebook_invitation_create', I'd like my InvitationsController instance to have a facebook member containing a create method - e.g.

class InvitationsController < ApplicationController
  def create
    ...
    # e.g. self.facebook.create
    self.send(params[:provider]).create
    ...
  end
end

Furthermore, I'd like the provider methods to not only function as if they were part of the controller itself - meaning they should have access to things like controller instance variables, params, session, etc. - but also to be (mostly) written as if they were part of the controller itself - meaning without any complex additional code as a result of being modularized.

I've created a simplified example below, in which MyClass has a greet method, which if called with a valid provider name (:facebook in this case), will call that providers greet method instead. In turn, the provider greet method accesses the message method of the including class, as if it were part of the class itself.

module Providers
  def facebook
    @facebook ||= FacebookProvider
  end

  module FacebookProvider
    class << self
      def greet
        proc {
          "#{message} from facebook!"
        }
      end
    end
  end
end

class MyClass
  include Providers
  attr_accessor :message

  def initialize(message="hello")
    self.message = message
  end

  def greet(provider=nil)
    (provider.nil? or !self.respond_to?(provider)) ? message : instance_exec(&self.send(provider).greet)
  end
end

This actually accomplishes almost everything I've previously stated, but I'm hung up on the fact that my provider functions need to be encapsulated in procs. I thought maybe I could simply call instance_exec on the method instead (after removing the proc encapsulation):

instance_exec(&self.send(provider).method(:greet))

...but then it seems like the instance_exec is ignored, as I get the error:

NameError: undefined local variable or method `message' for Providers::FacebookProvider:Module

Is there any way to call instance_exec on a defined method?

(I'm open to suggestions on how to better implement this as well...)

like image 914
Ryan Dugan Avatar asked Mar 21 '13 21:03

Ryan Dugan


1 Answers

I think this is simpler than you might expect (and I realize that my answer is 2 years after you asked)

You can use instance methods from modules and bind them to any object.

module Providers
  def facebook
    @facebook ||= FacebookProvider
  end

  module FacebookProvider
    def greet
      "#{message} from facebook!"
    end
  end
end

class MyClass
  include Providers
  attr_accessor :message

  def initialize(message="hello")
    self.message = message
  end

  def greet(provider=nil)
    if provider
      provider.instance_method(:greet).bind(self).call
    else
      message
    end
  end
end

If your provider is a module, you can user instance_method to create an UnboundMethod and bind it to the current self.

This is delegation. It's the basis for the casting gem which would work like this:

delegate(:greet, provider)

Or, if you opt-in to using method_missing from casting, your code could just look like this:

greet

But you'd need to set your delegate first:

class MyClass
  include Providers
  include Casting::Client
  delegate_missing_methods
  attr_accessor :message

  def initialize(message="hello", provider=facebook)
    cast_as(provider)
    self.message = message
  end
end

MyClass.new.greet # => "hello from facebook!"

I wrote about what delegation is and is not on my blog which is relevant to understanding DCI and what I wrote about in Clean Ruby

like image 60
Jim Gay Avatar answered Sep 25 '22 12:09

Jim Gay