在 Ruby 中,由于您可以包含多个 mixin,但只能扩展一个类,因此看起来 mixins 比继承更受欢迎。
我的问题:如果您正在编写必须扩展/包含才能有用的代码,您为什么要把它变成一个类?或者换一种说法,你为什么不总是把它做成一个模块呢?
我只能想到你想要一个类的一个原因,那就是你需要实例化这个类。然而,在 ActiveRecord::Base 的情况下,您永远不会直接实例化它。那么它不应该是一个模块吗?
在 Ruby 中,由于您可以包含多个 mixin,但只能扩展一个类,因此看起来 mixins 比继承更受欢迎。
我的问题:如果您正在编写必须扩展/包含才能有用的代码,您为什么要把它变成一个类?或者换一种说法,你为什么不总是把它做成一个模块呢?
我只能想到你想要一个类的一个原因,那就是你需要实例化这个类。然而,在 ActiveRecord::Base 的情况下,您永远不会直接实例化它。那么它不应该是一个模块吗?
我刚刚在The Well-Grounded Rubyist(顺便说一句,很棒的书)中读到了这个主题。作者的解释比我做得更好,所以我会引用他的话:
没有单一的规则或公式总能产生正确的设计。但是在做出类与模块的决定时,记住以下几点是很有用的:
模块没有实例。因此,实体或事物通常最好在类中建模,而实体或事物的特性或属性最好封装在模块中。相应地,如 4.1.1 节所述,类名往往是名词,而模块名通常是形容词(Stack 与 Stacklike)。
一个类只能有一个超类,但它可以混合任意数量的模块。如果您使用继承,请优先创建合理的超类/子类关系。不要用完一个类的唯一超类关系来赋予该类可能只是几组特征之一的东西。
在一个示例中总结这些规则,以下是您不应该做的事情:
module Vehicle
...
class SelfPropelling
...
class Truck < SelfPropelling
include Vehicle
...
相反,您应该这样做:
module SelfPropelling
...
class Vehicle
include SelfPropelling
...
class Truck < Vehicle
...
第二个版本更简洁地对实体和属性进行建模。卡车从 Vehicle 下降(这是有道理的),而 SelfPropelling 是车辆的一个特征(至少,我们在这个世界模型中关心的所有这些)——由于 Truck 是一个后代而传递给卡车的特性,或特殊形式的车辆。
我认为 mixins 是个好主意,但是这里还有一个没人提到的问题:命名空间冲突。考虑:
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
和module B
的常量和方法都在不太可能的命名空间中。问题是当发生碰撞时编译器根本不会警告你。
我认为这种行为不适用于大型程序员团队——你不应该假设实现的人class C
知道范围内的每个名称。Ruby 甚至可以让您覆盖不同类型的常量或方法。我不确定这是否可以被认为是正确的行为。
我的看法:模块用于共享行为,而类用于建模对象之间的关系。从技术上讲,您可以将所有内容都设为 Object 的实例,并混合您想要获得所需行为集的任何模块,但这将是一个糟糕、随意且相当不可读的设计。
您的问题的答案在很大程度上取决于上下文。提炼 pubb 的观察,选择主要由所考虑的域驱动。
是的,ActiveRecord 应该被包含而不是由子类扩展。另一个 ORM - datamapper - 正是实现了这一点!
我非常喜欢 Andy Gaskell 的回答——只是想补充一点,是的,ActiveRecord 不应该使用继承,而是包含一个模块来将行为(主要是持久性)添加到模型/类中。ActiveRecord 只是使用了错误的范例。
出于同样的原因,我非常喜欢 MongoId 而不是 MongoMapper,因为它让开发人员有机会使用继承作为对问题域中有意义的东西进行建模的方式。
遗憾的是,Rails 社区中几乎没有人按照应有的方式使用“Ruby 继承”——定义类层次结构,而不仅仅是添加行为。
我理解 mixin 的最佳方式是作为虚拟类。Mixin 是注入到类或模块的祖先链中的“虚拟类”。
当我们使用“include”并传递一个模块时,它会将模块添加到我们继承的类之前的祖先链中:
class Parent
end
module M
end
class Child < Parent
include M
end
Child.ancestors
=> [Child, M, Parent, Object ...
Ruby 中的每个对象也都有一个单例类。添加到这个单例类的方法可以直接在对象上调用,因此它们充当“类”方法。当我们在对象上使用“扩展”并将对象传递给模块时,我们正在将模块的方法添加到对象的单例类中:
module M
def m
puts 'm'
end
end
class Test
end
Test.extend M
Test.m
我们可以使用 singleton_class 方法访问单例类:
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
现在,我正在考虑template
设计模式。使用模块感觉不对。