在Ruby中,由于您可以包含多个mixin但只能扩展一个类,因此似乎mixins比继承更受青睐。
我的问题:如果您要编写必须扩展/包含的代码才能有用,那么为什么要将它设为类?或换种说法,为什么不总是将其设为模块?
我只能想到您想要一门课的一个原因,那就是是否需要实例化该课。但是,对于ActiveRecord :: Base,您永远不会直接实例化它。那么,它不应该是一个模块吗?
在Ruby中,由于您可以包含多个mixin但只能扩展一个类,因此似乎mixins比继承更受青睐。
我的问题:如果您要编写必须扩展/包含的代码才能有用,那么为什么要将它设为类?或换种说法,为什么不总是将其设为模块?
我只能想到您想要一门课的一个原因,那就是是否需要实例化该课。但是,对于ActiveRecord :: Base,您永远不会直接实例化它。那么,它不应该是一个模块吗?
Answers:
我刚刚在The Well-Grounded Rubyist(顺便说一句好书)中读到了这个话题。作者在解释方面比我做得更好,所以我引用他:
没有单一的规则或公式总能得出正确的设计。但是,在制定类与模块的决策时,记住一些注意事项很有用:
模块没有实例。因此,通常最好在类中对实体或事物建模,而最好将实体或事物的特性或属性封装在模块中。相应地,如第4.1.1节所述,类名通常是名词,而模块名通常是形容词(Stack vs Stacklike)。
一个类只能有一个超类,但是它可以根据需要混合任意数量的模块。如果您使用继承,则优先创建明智的超类/子类关系。不要用完一个类的唯一父类关系,使该类具有可能只是几组特征之一。
在一个示例中总结这些规则,这是您不应该做的:
module Vehicle
...
class SelfPropelling
...
class Truck < SelfPropelling
include Vehicle
...
相反,您应该这样做:
module SelfPropelling
...
class Vehicle
include SelfPropelling
...
class Truck < Vehicle
...
第二个版本对实体和属性建模更加整洁。卡车源自车辆(这是有道理的),而自我推进是车辆的特征(至少,在这个世界模型中我们所关心的所有车辆)–通过卡车是后代而传递给卡车的特征,或专用形式的车辆。
Truck
-IS A- Vehicle
没有Truck
那个不会Vehicle
。但是我可能会称呼模块SelfPropelable
(:?)嗯SelfPropeled
听起来不错,但它几乎是相同的:D。无论如何,我将不包括它Vehicle
,但Truck
-因为有未车辆SelfPropeled
。还有一个很好的迹象是要问-是否还有其他东西,不是ARE的车辆SelfPropeled
?-也许吧,但是我很难找到。因此,Vehicle
可以从类SelfPropelling继承(如类,将不适合作为SelfPropeled
-因为这是更多的角色)
我认为mixin是个好主意,但这里还有一个没人提及的问题:名称空间冲突。考虑:
module A
HELLO = "hi"
def sayhi
puts HELLO
end
end
module B
HELLO = "you stink"
def sayhi
puts HELLO
end
end
class C
include A
include B
end
c = C.new
c.sayhi
哪一个赢了?在Ruby中,事实证明是后者module B
,因为您在之后添加了它module A
。现在,很容易避免这个问题:确保module A
and module B
的所有常量和方法都位于不太可能的命名空间中。问题在于,发生冲突时,编译器根本不会警告您。
我认为这种行为无法扩展到大型的程序员团队中-您不应该假设实施人员class C
知道范围内的每个名称。Ruby甚至允许您重写常量或其他类型的方法。我不知道,可能曾经被认为是正确的行为。
C#sayhi
输出B::HELLO
不是因为Ruby混合了常量,而是因为ruby从更近到更远的地方解析了常量-因此HELLO
引用in B
总是会解析为B::HELLO
。即使C类定义了它自己的,这C::HELLO
也成立。
我的观点:模块用于共享行为,而类用于建模对象之间的关系。从技术上讲,您可以使所有内容都成为Object的实例,并混入想要获得所需行为集的任何模块中,但这将是一个糟糕,偶然且相当难以理解的设计。
您问题的答案很大程度上取决于具体情况。提炼pubb的观察结果,选择主要取决于所考虑的领域。
是的,ActiveRecord应该已经包括在内,而不是由子类扩展。另一个ORM- datamapper-正是实现了这一目标!
我了解mixin的最好方法是作为虚拟类。Mixins是已注入到类或模块的祖先链中的“虚拟类”。
当我们使用“ include”并将其传递给模块时,它将模块添加到祖先链中,该链就在我们要继承的类之前:
class Parent
end
module M
end
class Child < Parent
include M
end
Child.ancestors
=> [Child, M, Parent, Object ...
Ruby中的每个对象都有一个单例类。可以直接在对象上调用添加到此singleton类的方法,因此它们充当“类”方法。当我们在对象上使用“扩展”并将对象传递给模块时,我们会将模块的方法添加到对象的单例类中:
module M
def m
puts 'm'
end
end
class Test
end
Test.extend M
Test.m
我们可以使用singleton_class方法访问singleton类:
Test.singleton_class.ancestors
=> [#<Class:Test>, M, #<Class:Object>, ...
Ruby在将模块混合到类/模块中时为其提供了一些挂钩。included
是Ruby提供的钩子方法,只要您在某个模块或类中包含一个模块,就会调用该方法。就像包含的一样,有一个关联的extended
钩子用于扩展。当一个模块被另一个模块或类扩展时,它将被调用。
module M
def self.included(target)
puts "included into #{target}"
end
def self.extended(target)
puts "extended into #{target}"
end
end
class MyClass
include M
end
class MyClass2
extend M
end
这创建了一个有趣的模式,开发人员可以使用:
module M
def self.included(target)
target.send(:include, InstanceMethods)
target.extend ClassMethods
target.class_eval do
a_class_method
end
end
module InstanceMethods
def an_instance_method
end
end
module ClassMethods
def a_class_method
puts "a_class_method called"
end
end
end
class MyClass
include M
# a_class_method called
end
如您所见,该模块将添加实例方法,“类”方法,并直接作用于目标类(在这种情况下,调用a_class_method())。
ActiveSupport :: Concern封装了此模式。这是重写为使用ActiveSupport :: Concern的相同模块:
module M
extend ActiveSupport::Concern
included do
a_class_method
end
def an_instance_method
end
module ClassMethods
def a_class_method
puts "a_class_method called"
end
end
end