从 Ruby 中的模块/mixins 继承类方法

Posted

技术标签:

【中文标题】从 Ruby 中的模块/mixins 继承类方法【英文标题】:Inheriting class methods from modules / mixins in Ruby 【发布时间】:2012-05-28 09:48:29 【问题描述】:

众所周知,在 Ruby 中,类方法会被继承:

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works

然而,令我惊讶的是它不适用于 mixins:

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

我知道#extend 方法可以做到这一点:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

但我正在编写一个包含实例方法和类方法的 mixin(或者更确切地说,想编写):

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

现在我想做的是:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

我希望 A、B 从 Common 模块继承实例和类方法。但是,当然,这是行不通的。那么,难道没有一种秘密的方法可以让这种继承从单个模块中工作吗?

将它分成两个不同的模块,一个用于包含,另一个用于扩展,对我来说似乎是不雅的。另一种可能的解决方案是使用类Common 而不是模块。但这只是一种解决方法。 (如果有两组通用功能 Common1Common2 并且我们真的需要 mixins 怎么办?)类方法继承不能从 mixins 工作有什么深层次的原因吗?

【问题讨论】:

Is that possible to define a class method in a module?的可能重复 区别在于,在这里,我知道这是可能的——我要求以最不丑陋的方式来做这件事,以及天真的选择不起作用的原因。 随着更多的经验,我明白如果包含一个模块同时将模块方法添加到包含器的单例类中,Ruby 会过分猜测程序员的意图。这是因为“模块方法”实际上只是单例方法。模块对于具有单例方法并不是特别的,它们对于定义方法和常量的命名空间是特别的。命名空间与模块的单例方法完全无关,所以实际上单例方法的类继承比模块中没有它更令人惊讶。 【参考方案1】:

一个常见的习惯用法是使用included 钩子并从那里注入类方法。

module Foo
  def self.included base
    base.send :include, InstanceMethods
    base.extend ClassMethods
  end

  module InstanceMethods
    def bar1
      'bar1'
    end
  end

  module ClassMethods
    def bar2
      'bar2'
    end
  end
end

class Test
  include Foo
end

Test.new.bar1 # => "bar1"
Test.bar2 # => "bar2"

【讨论】:

include 添加实例方法,extend 添加类方法。这就是它的工作原理。我没有看到不一致之处,只有未达到的期望:) 我正在慢慢忍受这样一个事实,即您的建议与这个问题的实际解决方案一样优雅。但我很高兴知道为什么适用于类的东西不适用于模块。 @BorisStitnicky 相信这个答案。这是 Ruby 中的一个非常常见的习惯用法,可以准确地解决您询问的用例以及您遇到的原因。它可能看起来“不雅”,但这是您最好的选择。 (如果您经常这样做,您可以将 included 方法定义移动到另一个模块,并将其包含在您的主模块中;) 阅读this thread 了解更多关于“为什么?”的信息。 @werkshy:将模块包含在一个虚拟类中。【参考方案2】:

这是完整的故事,解释了理解为什么模块包含在 Ruby 中的工作方式所需的元编程概念。

包含模块时会发生什么?

将模块包含到类中会将模块添加到类的祖先中。您可以通过调用其ancestors 方法查看任何类或模块的祖先:

module M
  def foo; "foo"; end
end

class C
  include M

  def bar; "bar"; end
end

C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
#       ^ look, it's right here!

当您在C 的实例上调用方法时,Ruby 将查看此祖先列表的每一项,以便找到具有提供名称的实例方法。由于我们将M 包含在C 中,M 现在是C 的祖先,所以当我们在C 的实例上调用foo 时,Ruby 会在M 中找到该方法:

C.new.foo
#=> "foo"

请注意,包含不会将任何实例或类方法复制到类中——它只是向类添加一个“注释”,它还应该在包含的模块中查找实例方法。

我们模块中的“类”方法呢?

因为包含只会改变派发实例方法的方式,将模块包含到类中只会使其实例方法在该类上可用。模块中的“类”方法和其他声明不会自动复制到类中:

module M
  def instance_method
    "foo"
  end

  def self.class_method
    "bar"
  end
end

class C
  include M
end

M.class_method
#=> "bar"

C.new.instance_method
#=> "foo"

C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class

Ruby 是如何实现类方法的?

在 Ruby 中,类和模块是普通对象——它们是类 ClassModule 的实例。这意味着您可以动态创建新类,将它们分配给变量等:

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

同样在 Ruby 中,您可以在对象上定义所谓的单例方法。这些方法作为新的实例方法添加到对象的特殊隐藏单例类中:

obj = Object.new

# define singleton method
def obj.foo
  "foo"
end

# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]

但是类和模块不也只是普通的对象吗?事实上他们是!这是否意味着他们也可以有单例方法?是的,它确实!这就是类方法的诞生方式:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

或者,更常见的定义类方法的方法是在类定义块中使用self,它指的是正在创建的类对象:

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

如何在模块中包含类方法?

正如我们刚刚建立的,类方法实际上只是类对象的单例类上的实例方法。这是否意味着我们可以在单例类中包含一个模块来添加一堆类方法?是的,确实如此!

module M
  def new_instance_method; "hi"; end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M
  self.singleton_class.include M::ClassMethods
end

HostKlass.new_class_method
#=> "hello"

self.singleton_class.include M::ClassMethods 这行看起来不太好,所以 Ruby 添加了 Object#extend,它的作用相同——即在对象的单例类中包含一个模块:

class HostKlass
  include M
  extend M::ClassMethods
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ there it is!

extend 调用移动到模块中

前面的示例不是结构良好的代码,原因有两个:

    我们现在必须在 HostClass 定义中调用 both includeextend 才能正确包含我们的模块。如果您必须包含许多类似的模块,这可能会变得非常麻烦。 HostClass 直接引用M::ClassMethods,这是M 模块的实现细节HostClass 不需要知道或关心。

那么怎么样:当我们在第一行调用include 时,我们以某种方式通知模块它已被包含,并且还给它我们的类对象,以便它可以调用extend 本身。这样,模块的工作就是根据需要添加类方法。

这正是 特殊的self.included 方法 的用途。每当模块被包含到另一个类(或模块)中时,Ruby 会自动调用此方法,并将宿主类对象作为第一个参数传入:

module M
  def new_instance_method; "hi"; end

  def self.included(base)  # `base` is `HostClass` in our case
    base.extend ClassMethods
  end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M

  def self.existing_class_method; "cool"; end
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ still there!

当然,添加类方法并不是我们在self.included 中唯一可以做的事情。我们有类对象,所以我们可以在它上面调用任何其他(类)方法:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end

【讨论】:

精彩的答案!经过一天的挣扎,终于能够理解这个概念。谢谢。 我认为这可能是我在 SO 上见过的最好的书面答案。感谢您提供令人难以置信的清晰度并扩展了我对 Ruby 的理解。如果我能给这个 100pt 奖金,我会的!【参考方案3】:

正如 Sergio 在 cmets 中提到的,对于已经在 Rails 中的人(或者不介意依赖 Active Support),Concern 在这里很有帮助:

require 'active_support/concern'

module Common
  extend ActiveSupport::Concern

  def instance_method
    puts "instance method here"
  end

  class_methods do
    def class_method
      puts "class method here"
    end
  end
end

class A
  include Common
end

【讨论】:

【参考方案4】:

你可以通过这样做得到你的蛋糕并吃掉它:

module M
  def self.included(base)
    base.class_eval do # do anything you would do at class level
      def self.doit #class method
        @@fred = "Flintstone"
        "class method doit called"
      end # class method define
      def doit(str) #instance method
        @@common_var = "all instances"
        @instance_var = str
        "instance method doit called"
      end
      def get_them
        [@@common_var,@instance_var,@@fred]
      end
    end # class_eval
  end # included
end # module

class F; end
F.include M

F.doit  # >> "class method doit called"
a = F.new
b = F.new
a.doit("Yo") # "instance method doit called"
b.doit("Ho") # "instance method doit called"
a.get_them # >> ["all instances", "Yo", "Flintstone"]
b.get_them # >> ["all instances", "Ho", "Flintstone"]

如果你打算添加实例和类变量,你最终会陷入困境,因为你会遇到一堆损坏的代码,除非你这样做。

【讨论】:

在传递 class_eval 块时,有一些奇怪的事情不起作用,例如定义常量、定义嵌套类以及在方法之外使用类变量。为了支持这些东西,你可以给 class_eval 一个heredoc(字符串)而不是一个块:base.class_eval

以上是关于从 Ruby 中的模块/mixins 继承类方法的主要内容,如果未能解决你的问题,请参考以下文章

oop思维意识,类 模块命名空间,类扩展之继承 组合mixin三种模式

怎么理解mixins

Ruby 中的私有模块方法

Mixin覆盖继承的方法

面向对象高级编程

检索所有包含特定 mixin 的类定义的正确方法是啥?