在Ruby中继承modules / mixins中的类方法
众所周知,在Ruby中,类方法被继承:
class P
def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works
但是,对于我来说,它并不适用于mixin:
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
而不是模块。 但这只是一个解决方法。 (如果有两组常用的功能Common1
和Common2
,我们真的需要mixin吗?)为什么类方法继承不能在mixin中工作?
一个常见的习惯用法是使用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"
这里是完整的故事,解释了必要的元编程概念,以理解为什么模块包含在Ruby中的工作方式。
包含模块时会发生什么?
将模块包含到类中会将该模块添加到该类的祖先中。 您可以通过调用其ancestors
方法来查看任何类或模块的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中,类和模块是普通对象 - 它们是Class
和Module
类的实例。 这意味着你可以动态地创建新的类,将它们分配给变量等。
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
,它也是这样做的 - self.singleton_class.include M::ClassMethods
一个模块包含到对象的单例类中:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
将extend
调用移入模块
前面的例子不是结构良好的代码,原因有两个:
HostClass
定义中调用include
和extend
来正确地包含我们的模块。 如果你必须包含许多类似的模块,这会变得非常麻烦。 HostClass
直接引用M::ClassMethods
,它是HostClass
不需要知道或关心的模块M
的实现细节。 那么如何做到这一点:当我们在第一行调用include
时,我们以某种方式通知模块它已被包含在内,并且也将它赋给我们的类对象,以便它可以调用extend
。 这样,如果需要的话,模块的工作就是添加类方法。
这正是特殊的self.included
方法的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
正如Sergio在评论中提到的那样,对于已经在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
链接地址: http://www.djcxy.com/p/44843.html