如何在Ruby中使用模块覆盖静态类方法?

时间:2012-02-03 12:14:27

标签: ruby

module Imodule
  ???
end

class Some
  include Imodule

  def self.imethod
    puts "original"
  end
end

Some.imethod
# => "overrided"

如何创建一个覆盖静态方法的模块?

这是一个深入了解ruby功能的访谈问题。不建议另一个问题的表述:)

4 个答案:

答案 0 :(得分:30)

好的,这是一个有效的代码。请注意,您甚至不必触及目标类! :)

class Klass
  def self.say
    puts 'class'
  end
end

module FooModule
  def self.included base
    base.instance_eval do
      def say
        puts "module"
      end
    end
  end
end


Klass.send(:include, FooModule)

Klass.say

说明

现在,在类方法中混合的经典方法就是这样(当然,它并没有解决问题)。

module FooModule
  def self.included base
    base.extend ClassMethods
  end

  module ClassMethods
    def bar
      puts "module"
    end
  end
end

class Klass
  include FooModule

  def self.bar
    puts 'class'
  end
end


Klass.bar #=> class

当模块被包含或扩展到类中时,其方法就位于继承链中此类的方法的正上方。这意味着如果我们在该类方法中调用super,它将打印“module”。但我们不想触及原始类定义,我们希望从外部改变它。

那么,我们可以做些什么吗?

对我们有好处,红宝石有一个"open classes"的概念。这意味着我们几乎可以更改应用程序中的所有内容,甚至是某些第三方库。每个类都可以“打开”,并且可以向其添加新方法,或者可以重新定义旧方法。让我们来看看它是如何工作的。

class Klass
  def self.bar
    puts 'class'
  end
end

class Klass
  def self.bar
    puts 'class 2'
  end
end

Klass.bar #=> class 2

第二个类定义不会覆盖前一个定义,它会打开并改变它。在这种情况下,碰巧定义了一个具有相同名称的方法。这导致旧方法被新方法覆盖。这适用于任何类,甚至是基类库。

puts [1, 2, 3].to_s #=> [1, 2, 3]

class Array
  def to_s
    "an array: #{join ', '}"
  end
end

puts [1, 2, 3].to_s #=> an array: 1, 2, 3

或者相同的代码可以重写为

puts [1, 2, 3].to_s #=> [1, 2, 3]

Array.class_eval do
  def to_s
    "an array: #{join ', '}"
  end
end

puts [1, 2, 3].to_s #=> an array: 1, 2, 3

应用知识

让我们从更简单的事情开始,比如重写实例方法。

class Klass
  def say
    puts 'class'
  end
end

module FooModule
  def self.included base
    base.class_eval do
      def say
        puts "module"
      end
    end
  end
end


Klass.send(:include, FooModule)

Klass.new.say #=> module

模块有一个特殊的回调,每次模块包含在类中时都会调用它。我们可以使用它来调用该类的class_eval并重新定义一个方法。

以类似的方式替换类方法。

class Klass
  def self.say
    puts 'class'
  end
end

module FooModule
  def self.included base
    base.instance_eval do
      def say
        puts "module"
      end
    end
  end
end


Klass.send(:include, FooModule)

Klass.say #=> module

这里唯一的区别是我们调用instance_eval而不是class_eval。这可能是一个非常令人困惑的部分。简而言之,class_eval创建实例方法,instance_eval创建类方法。

这取自我的blog post

答案 1 :(得分:4)

如果您需要在新方法中调用刚刚覆盖的原始方法,该怎么办?

换句话说,如果您希望能够从覆盖的类方法调用super,则可以采用与覆盖时调用super相同的方式实例方法

这是我最终遇到的解决方案,以防其他人发现它有用:

class Klass
  def self.say
    puts 'original, '
  end
end

module FooModule
  def self.included base
    orig_method = base.method(:say)
    base.define_singleton_method :say do
      orig_method.call
      puts "module"
    end
  end
end


class Klass
  include FooModule
end

Klass.say  # => original, module

我们必须使用define_method而不是def,这样我们就可以在新方法定义中创建一个闭包并访问局部变量(在本例中是原始方法的保存版本)

顺便说一下,

base.define_singleton_method :say do

相当于做

(class << base; self; end).send :define_method, :say do

特别感谢Ruby singleton methods with (class_eval, define_method) vs (instance_eval, define_method)http://yugui.jp/articles/846教育我并指出我正确的方向。

答案 2 :(得分:1)

如果这个真的是一个面试问题来测试对Ruby特性的深刻理解,那么答案是微不足道的:Ruby没有类方法。它也没有静态方法。它肯定没有静态类方法。如果您对Ruby功能有深入的了解,那么,即使您只是对Ruby有一种肤浅的熟悉,您也知道这一点。这是一个棘手的问题。

答案 3 :(得分:0)

覆盖类方法的最简单方法是将模块prependsingletonSome 类:

module Imodule
  def imethod
    puts "overrided"
  end
end

Some.singleton_class.prepend(Imodule)

Some.imethod
# => "overrided"

说明:extend 对类方法的作用与 include 对实例方法的作用相同。 extendinclude 都在类主体中声明的方法之前解析,因此它们永远不会被覆盖。

为了覆盖实例方法,我们使用 prepend 但是对于类方法没有 prepend 的替代方案。由于在 Ruby 中一切都是对象,因此您可以做的是在将类视为对象实例时使用 prepend。因为类方法本质上是在对象单例类中声明的对象实例方法,所以可以打开对应的单例类,prepend方法。

你也可以在课堂上做同样的事情:

class Some
  class << self
    prepend Imodule
  end
end

Some.imethod
# => "overrided"