Ruby 继承 VS Mixins

在 Ruby 中,由于可以包含多个 Mixin,但只能扩展一个类,因此看起来混合类似乎比继承更受欢迎。

我的问题是: 如果你编写的代码必须扩展/包含才有用,为什么还要把它变成一个类呢?或者换句话说,你为什么不把它变成一个模块呢?

我只能想到一个需要类的原因,那就是需要实例化类。但是,对于 ActiveRecord: : Base,您从不直接实例化它。所以它不应该是个模块吗?

37728 次浏览

现在,我正在考虑 template的设计模式。它只是感觉不适合模块。

我在 一个扎实的红宝石爱好者(顺便说一句,这是一本很棒的书)中读到过这个话题。作者在解释方面比我做得更好,所以我引用他的话:


没有一个单一的规则或公式总是导致正确的设计 在进行类对模块的决策时,需要考虑以下几点:

  • 模块没有实例。实体或事物通常是最好的 在类中建模,实体或事物的特征或属性是 最好封装在模块中。相应地,如第4.1.1节所述,类 名称往往是名词,而模块名称往往是形容词(堆栈 相对于斯塔克利克)

  • 一个类可以只有一个超类,但是它可以混合任意多的模块 你在使用继承,优先创建一个合理的超类/子类 不要用尽一个类唯一的超类关系 赋予这个类可能只是几组特征中的一组

用一个例子总结这些规则,下面是你不应该做的:

module Vehicle
...
class SelfPropelling
...
class Truck < SelfPropelling
include Vehicle
...

相反,你应该这样做:

module SelfPropelling
...
class Vehicle
include SelfPropelling
...
class Truck < Vehicle
...

第二个版本的实体和属性建模更加整洁 自动推进是汽车的后代(这是有道理的) ,而自动推进是汽车的一个特征(至少是我们在这个世界模型中所关心的所有特征)ーー这个特征通过云梯车的后代或专业化传递给了卡车 车辆形式。

我的看法是: 模块用于共享行为,而类用于建模对象之间的关系。从技术上讲,您可以将所有东西都变成 Object 的一个实例,然后混合到您希望获得所需行为集的任何模块中,但那将是一个糟糕、随意和相当不可读的设计。

你问题的答案很大程度上是基于上下文的。从 pubb 的观察来看,选择主要是由考虑的领域驱动的。

是的,ActiveRecord 应该被包含而不是被子类扩展。另一个 ORM-数据采集器-正是实现这一点!

我非常喜欢 Andy Gaskell 的回答——只是想补充一下,是的,ActiveRecord 不应该使用继承,而是应该包含一个模块来向模型/类添加行为(主要是持久性)。ActiveRecord 只是使用了错误的范例。

出于同样的原因,我非常喜欢 MongoId 而不是 MongoMapper,因为它让开发人员有机会使用继承作为在问题领域建模有意义内容的方法。

令人遗憾的是,Rails 社区中几乎没有人使用“ Ruby 继承”来定义类层次结构,而不仅仅是添加行为。

我认为混合是一个很好的主意,但是这里还有一个没有人提到的问题: 名称空间冲突:

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 Amodule B的常量和方法都在不太可能的名称空间中。问题是,当发生冲突时,编译器根本不会提醒您。

我认为这种行为不适用于大型的程序员团队——您不应该假设实现 class C的人知道范围内的每个名称。Ruby 甚至允许您重写 不同的类型的常量或方法。我不确定 永远不会可以被认为是正确的行为。

我理解混合的最好方式是将其作为虚类。混合类是“虚拟类”,它们被注入到类或模块的祖先链中。

当我们使用“ 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 方法访问 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

可以看到,这个单独的模块添加了实例方法、“ class”方法,并直接作用于目标类(在本例中调用 a _ class _ method ())。

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