4

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!

Nathan
  • 7,627
  • 11
  • 46
  • 80

4 Answers4

12

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.

Eric Walker
  • 7,063
  • 3
  • 35
  • 38
  • 1
    Rails has a method called `alias_method_chain` which helps with this. – Jeremy Roman Dec 10 '12 at 18:20
  • Nice. Pedantic question: is it monkey patching if done in the module that's getting mixed into the class? – Nathan Dec 10 '12 at 20:06
  • @Nathan: I don't think "monkey patching" has a precise a definition. With Ruby, there's metaprogramming, which is just the normal way of doing things (like using templates in C++), and then there's extreme forms of metaprogramming, which become clever or aggressive and modify in unclear ways things that have already been defined. If you're being *too* clever, it's probably the bad kind of monkey patching. If you're just trying to solve a problem, as in your case, it might be acceptable monkey patching. The Rails guys use `alias_method_chain`, for example, on a regular basis. – Eric Walker Dec 10 '12 at 21:11
9

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?

Community
  • 1
  • 1
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • I just added a comment on your answer in the link above. I think alias_method can be used carefully to provide a reasonable alternative to super. Thanks for pointing out the Module#prepend method on the horizon of Ruby 2.0 -- very nice! – Nathan Dec 10 '12 at 20:22
1

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.

BernardK
  • 3,674
  • 2
  • 15
  • 10
  • The problem I'm running into now is that once the class is overwritten (even if conditionally as you've described), the next request uses the overwritten class in production. In other words, the class is mutated and needs to be reloaded, but in production reloading classes is ab resource hit. – Nathan Dec 14 '12 at 17:19
  • @Nathan : 1) The class is not overwritten, an old method is aliased and a new method is defined 2) I don't understand "production" and "reload". Ruby is dynamic and changes happen on the fly. – BernardK Dec 15 '12 at 12:25
  • I forgot to mention that this will ultimately be for a Rails project, so when I mentioned production, I was referring to the production mode Rails runs in -- where classes are loaded once into the application instance. Once loaded, any changes to the classes persist, unless you reload the original class (i.e. restart the application or configure some other relatively costly option to reload classes). – Nathan Dec 15 '12 at 20:55
0

[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.

BernardK
  • 3,674
  • 2
  • 15
  • 10
  • Err, my bad. I meant class_eval when I wrote this. I'll try to edit so it will be clear. My apologies for the confusion. Thanks for the catch. And no, I'm not subclassing at all, so it doesn't work as you described. The way I'm getting it to work is with `alias_method`. – Nathan Dec 10 '12 at 22:46