3

我有一个应用程序,其中包含模块到核心类中,用于添加客户端自定义。

我发现 class_eval 是覆盖核心类中方法的好方法,但有时我想避免重写整个方法,而只是遵循原始方法。

例如,如果我有一个名为 的方法account_balance,最好在我的模块(即包含在类中的模块)中执行类似的操作:

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

但是使用 class_eval 似乎将该super方法从查找路径中取出。

有谁知道如何解决这个问题?

谢谢!

4

4 回答 4

12

我认为有几种方法可以做你想做的事。一种是打开类并为旧实现起别名:

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 

这是猴子修补的一种形式,因此最好适度使用该成语。此外,有时需要一个单独的辅助方法来保存通用功能。

编辑:有关更全面的选项集,请参阅 Jörg W Mittag 的答案。

于 2012-12-10T18:18:15.357 回答
9

我发现 instance_eval 是覆盖核心类中方法的好方法,

你没有压倒一切。你正在覆盖又名猴子补丁。

但有时我想避免重写整个方法,而只是遵循原始方法。

你不能遵循原来的方法。没有原始方法。你覆盖了它。

但是使用 instance_eval 似乎将该super方法从查找路径中取出。

您的示例中没有继承。super甚至没有发挥作用。

请参阅此答案以获取可能的解决方案和替代方案:猴子修补方法时,您可以从新实现中调用覆盖的方法吗?

于 2012-12-10T19:30:41.190 回答
1

正如你所说, alias_method 必须小心使用。鉴于这个人为的例子:

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

你最终会出现一个无限循环,因为在别名之后,old_account_balance 是 account_balance 的副本,它现在调用自己:

$ 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)

[来自 Pickaxe] 这种技术 [alias_method] 的问题在于,您依赖的不是名为 old_xxx 的现有方法。一个更好的选择是使用实际上是匿名的方法对象。

话虽如此,如果您拥有源代码,一个简单的别名就足够了。但对于更一般的情况,我将使用 Jörg 的方法包装技术。

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

输出 :

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

为什么不是类变量 @@is_defined_account_balance ?[来自 Pickaxe] 包含 include 的模块或类定义可以访问它所包含的模块的常量、类变量和实例方法。
它将避免将其从 CustomClient 传递到主机并简化测试:

    old_account_balance if @@is_defined_account_balance # = super

但是有些人不喜欢类变量和全局变量一样。

于 2012-12-11T12:18:25.490 回答
0

[来自 Pickaxe] 方法 Object#instance_eval 允许您将 self 设置为任意对象,评估块中的代码,然后重置 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

如您所见,def account_balance在包含模块的主机类 Client 的上下文中进行评估,因此 account_balance 成为 Client 的单例方法(又名类方法):

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

Client.new.account_balance 不起作用,因为它不是实例方法。

“我有一个将模块包含到核心类中的应用程序”

由于您没有提供太多细节,因此我设想了以下基础架构:

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

class Client < SuperClient
    include CustomClient
end

现在用 class_eval 替换 instance_eval。[来自 Pickaxe] class_eval 将事情设置为就像您在类定义的主体中一样,因此方法定义将定义实例方法。

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

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

输出 :

  #=> 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已经奏效了。问题是instance_eval。

于 2012-12-10T21:39:05.760 回答