【问题标题】:Using super with class_eval将 super 与 class_eval 一起使用
【发布时间】:2012-12-10 18:08:49
【问题描述】:

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

我发现 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 方法从查找路径中取出。

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

谢谢!

【问题讨论】:

    标签: ruby metaprogramming


    【解决方案1】:

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

    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örg W Mittag 的答案以获得更全面的选项。

    【讨论】:

    • Rails 有一个名为 alias_method_chain 的方法可以帮助解决这个问题。
    • 不错。迂腐的问题:如果在混入课堂的模块中完成,是猴子修补吗?
    • @Nathan:我不认为“猴子补丁”有一个精确的定义。在 Ruby 中,有元编程,这只是正常的做事方式(例如在 C++ 中使用模板),然后是极端形式的元编程,它变得聪明或激进,并以不清楚的方式修改已经定义的事物。如果您 聪明,那可能是猴子修补的糟糕类型。如果你只是想解决一个问题,就像你的情况一样,它可能是可以接受的猴子补丁。例如,Rails 人员经常使用alias_method_chain
    【解决方案2】:

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

    你没有压倒一切。您正在覆盖,也就是monkeypatching。

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

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

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

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

    有关可能的解决方案和替代方案,请参阅此答案:When monkey patching a method, can you call the overridden method from the new implementation?

    【讨论】:

    • 我刚刚在上面的链接中添加了对您的回答的评论。我认为 alias_method 可以谨慎使用,为 super 提供合理的替代方案。感谢您指出 Ruby 2.0 即将推出的 Module#prepend 方法——非常好!
    【解决方案3】:

    正如您所说,必须谨慎使用 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
    

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

    【讨论】:

    • 我现在遇到的问题是,一旦类被覆盖(即使如您所描述的那样有条件),下一个请求在生产中使用覆盖的类。换句话说,类发生了变异,需要重新加载,但在生产环境中,重新加载类会造成资源损失。
    • @Nathan : 1) 类没有被覆盖,旧方法被别名并定义了新方法 2) 我不懂“生产”和“重新加载”。 Ruby 是动态的,变化是即时发生的。
    • 我忘了提到这最终将用于 Rails 项目,所以当我提到生产时,我指的是 Rails 运行的生产模式——其中类被加载到应用程序实例中一次。加载后,对类的任何更改都会保留,除非您重新加载原始类(即重新启动应用程序或配置一些其他相对昂贵的选项来重新加载类)。
    【解决方案4】:

    [来自 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
    

    现在将 instance_eval 替换为 class_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。

    【讨论】:

    • 呃,我的错。当我写这篇文章时,我的意思是 class_eval 。我会尝试编辑,所以它会很清楚。我为混乱道歉。谢谢你的收获。不,我根本没有继承,所以它不像你描述的那样工作。我让它工作的方式是使用alias_method
    猜你喜欢
    • 2010-12-21
    • 2015-07-23
    • 2011-02-18
    • 2012-05-05
    • 2011-03-01
    • 1970-01-01
    • 2011-05-23
    • 2011-04-01
    • 1970-01-01
    相关资源
    最近更新 更多