我有一个应用程序,其中包含用于添加客户端自定义的核心类的模块。
我发现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
方法从查找路径中取出。
有谁知道如何解决这个问题?
谢谢!
答案 0 :(得分:10)
我认为有几种方法可以做你想做的事情。一种是打开类并为旧实现添加别名:
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
这是monkey patching的一种形式,所以最好还是适度使用这个成语。此外,有时需要的是一个独立的辅助方法,它具有通用功能。
编辑:有关更全面的选项,请参阅JörgWMittag的答案。
答案 1 :(得分:9)
我发现instance_eval是覆盖核心类中的方法的好方法,
你没有压倒一切。你覆盖又称monkeypatching。
但有时我想避免重写整个方法,只需遵循原始方法。
您无法遵循原始方法。没有原创方法。你覆盖了它。
但是使用instance_eval似乎将
super
方法从查找路径中删除。
您的示例中没有继承。 super
甚至没有发挥作用。
有关可能的解决方案和替代方案,请参阅此答案:When monkey patching a method, can you call the overridden method from the new implementation?
答案 2 :(得分: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的Method Wrapping技术。
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
但有些人不喜欢类变量和全局变量。
答案 3 :(得分: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类的上下文中进行评估,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。