Nathan
Nathan

Reputation: 7855

Using super with class_eval

I have an app that includes modules into core Classes for adding client customizations.

I'm finding that class_eval is a good way to override methods in the core Class, but sometimes I would like to avoid re-writing the entire method, and just defer to the original method.

For example, if I have a method called account_balance, it would be nice to do something like this in my module (i.e. the module that gets included into the Class):

module CustomClient
  def self.included base
    base.class_eval do
      def account_balance
        send_alert_email if balance < min
        super # Then this would just defer the rest of the logic defined in the original class
      end
    end
  end
end

But using class_eval seems to take the super method out of the lookup path.

Does anyone know how to work around this?

Thanks!

Upvotes: 4

Views: 6989

Answers (4)

BernardK
BernardK

Reputation: 3734

As you say, alias_method must be used carefully. Given this contrived example :

module CustomClient
...    
    host.class_eval do
      alias :old_account_balance :account_balance
      def account_balance ...
        old_account_balance
      end
...
class CoreClass
    def old_account_balance ... defined here or in a superclass or
                                in another included module
    def account_balance
        # some new stuff ...
        old_account_balance # some old stuff ...
    end
    include CustomClient
end

you end up with an infinite loop because, after alias, old_account_balance is a copy of account_balance, which now calls itself :

$ ruby -w t4.rb 
t4.rb:21: warning: method redefined; discarding old old_account_balance
t4.rb:2: warning: previous definition of old_account_balance was here
[ output of puts removed ]
t4.rb:6: stack level too deep (SystemStackError)

[from the Pickaxe] The problem with this technique [alias_method] is that you’re relying on there not being an existing method called old_xxx. A better alternative is to make use of method objects, which are effectively anonymous.

Having said that, if you own the source code, a simple alias is good enough. But for a more general case, i'll use Jörg's Method Wrapping technique.

class CoreClass
    def account_balance
        puts 'CoreClass#account_balance, stuff deferred to the original method.'
    end
end

module CustomClient
  def self.included host
    @is_defined_account_balance = host.new.respond_to? :account_balance
    puts "is_defined_account_balance=#{@is_defined_account_balance}"
        # pass this flag from CustomClient to host :
    host.instance_variable_set(:@is_defined_account_balance,
                                @is_defined_account_balance)
    host.class_eval do
      old_account_balance = instance_method(:account_balance) if
                @is_defined_account_balance
      define_method(:account_balance) do |*args|
        puts 'CustomClient#account_balance, additional stuff'
            # like super :
        old_account_balance.bind(self).call(*args) if
                self.class.instance_variable_get(:@is_defined_account_balance)
      end
    end
  end
end

class CoreClass
    include CustomClient
end

print 'CoreClass.new.account_balance : '
CoreClass.new.account_balance

Output :

$ ruby -w t5.rb 
is_defined_account_balance=true
CoreClass.new.account_balance : CustomClient#account_balance, additional stuff
CoreClass#account_balance, stuff deferred to the original method.

Why not a class variable @@is_defined_account_balance ? [from the Pickaxe] The module or class definition containing the include gains access to the constants, class variables, and instance methods of the module it includes.
It would avoid passing it from CustomClient to host and simplify the test :

    old_account_balance if @@is_defined_account_balance # = super

But some dislike class variables as much as global variables.

Upvotes: 1

BernardK
BernardK

Reputation: 3734

[from the Pickaxe] The method Object#instance_eval lets you set self to be some arbitrary object, evaluates the code in a block with, and then resets self.

module CustomClient
  def self.included base
    base.instance_eval do
      puts "about to def account_balance in #{self}"
      def account_balance
        super
      end
    end
  end
end

class Client
    include CustomClient #=> about to def account_balance in Client
end

As you can see, def account_balance is evaluated in the context of class Client, the host class which includes the module, hence account_balance becomes a singleton method (aka class method) of Client :

print 'Client.singleton_methods : '
p Client.singleton_methods #=> Client.singleton_methods : [:account_balance]

Client.new.account_balance won't work because it's not an instance method.

"I have an app that includes modules into core Classes"

As you don't give much details, I have imagined the following infrastructure :

class SuperClient
    def account_balance
        puts 'SuperClient#account_balance'
    end
end

class Client < SuperClient
    include CustomClient
end

Now replace instance_eval by class_eval. [from the Pickaxe] class_eval sets things up as if you were in the body of a class definition, so method definitions will define instance methods.

module CustomClient
...
   base.class_eval do
...

print 'Client.new.account_balance : '
Client.new.account_balance

Output :

  #=> from include CustomClient :
about to def account_balance in Client #=> as class Client, in the body of Client
Client.singleton_methods : []
Client.new.account_balance : SuperClient#account_balance #=> from super


"But using instance_eval seems to take the super method out of the lookup path."

super has worked. The problem was instance_eval.

Upvotes: 0

Eric Walker
Eric Walker

Reputation: 7571

I think there are several ways to do what you're wanting to do. One is to open the class and alias the old implementation:

class MyClass
  def method1
    1
  end
end

class MyClass
  alias_method :old_method1, :method1
  def method1
    old_method1 + 1
  end
end

MyClass.new.method1
 => 2 

This is a form of monkey patching, so probably best to make use of the idiom in moderation. Also, sometimes what is wanted is a separate helper method that holds the common functionality.

EDIT: See Jörg W Mittag's answer for a more comprehensive set of options.

Upvotes: 12

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369458

I'm finding that instance_eval is a good way to override methods in the core Class,

You are not overriding. You are overwriting aka monkeypatching.

but sometimes I would like to avoid re-writing the entire method, and just defer to the original method.

You can't defer to the original method. There is no original method. You overwrote it.

But using instance_eval seems to take the super method out of the lookup path.

There is no inheritance in your example. super doesn't even come into play.

See this answer for possible solutions and alternatives: When monkey patching a method, can you call the overridden method from the new implementation?

Upvotes: 9

Related Questions