红宝石继承vs mixins


127

在Ruby中,由于您可以包含多个mixin但只能扩展一个类,因此似乎mixins比继承更受青睐。

我的问题:如果您要编写必须扩展/包含的代码才能有用,那么为什么要将它设为类?或换种说法,为什么不总是将其设为模块?

我只能想到您想要一门课的一个原因,那就是是否需要实例化该课。但是,对于ActiveRecord :: Base,您永远不会直接实例化它。那么,它不应该是一个模块吗?

Answers:


176

刚刚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 
... 

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


1
这个例子很好地展示了它-卡车是一辆车-没有卡车会成为一辆车。
PL J

1
这个例子很好地展示了Truck-IS A- Vehicle没有Truck那个不会Vehicle。但是我可能会称呼模块SelfPropelable(:?)嗯SelfPropeled听起来不错,但它几乎是相同的:D。无论如何,我将不包括它Vehicle,但Truck-因为有未车辆SelfPropeled。还有一个很好的迹象是要问-是否还有其他东西,不是ARE的车辆SelfPropeled?-也许吧,但是我很难找到。因此,Vehicle可以从类SelfPropelling继承(如类,将不适合作为SelfPropeled-因为这是更多的角色)
PLĴ

39

我认为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 Aand module B的所有常量和方法都位于不太可能的命名空间中。问题在于,发生冲突时,编译器根本不会警告您。

我认为这种行为无法扩展到大型的程序员团队中-您不应该假设实施人员class C知道范围内的每个名称。Ruby甚至允许您重写常量或其他类型的方法。我不知道,可能曾经被认为是正确的行为。


2
这是一个明智的警告。让人想起C ++中的多重继承陷阱。
克里斯·汤金森

1
有什么好的缓解方法吗?这似乎是为什么Python多重继承是一种出色的解决方案的原因(不尝试启动一种语言的自选匹配;只是比较此特定功能)。
Marcin 2015年

1
@bazz太好了,但大多数语言的编写都很麻烦。它也与鸭式语言最相关。这也不保证您不会得到怪异的状态。
Marcin's

我知道是旧帖子,但仍然可以在搜索中找到。答案部分不正确- C#sayhi输出B::HELLO不是因为Ruby混合了常量,而是因为ruby从更近到更远的地方解析了常量-因此HELLO引用in B总是会解析为B::HELLO。即使C类定义了它自己的,这C::HELLO也成立。
Laas

13

我的观点:模块用于共享行为,而类用于建模对象之间的关系。从技术上讲,您可以使所有内容都成为Object的实例,并混入想要获得所需行为集的任何模块中,但这将是一个糟糕,偶然且相当难以理解的设计。


2
这以一种直接的方式回答了这个问题:继承强制执行一种特定的组织结构,该结构可以使您的项目更具可读性。
emery 2016年

10

您问题的答案很大程度上取决于具体情况。提炼pubb的观察结果,选择主要取决于所考虑的领域。

是的,ActiveRecord应该已经包括在内,而不是由子类扩展。另一个ORM- datamapper-正是实现了这一目标!


4

我非常喜欢Andy Gaskell的答案-只是想补充一点,ActiveRecord不应使用继承,而应包括一个将行为(主要是持久性)添加到模型/类的模块。ActiveRecord只是使用了错误的范例。

出于同样的原因,我非常喜欢MongoId,而不是MongoMapper,因为它使开发人员有机会使用继承作为对问题域中有意义的事物进行建模的方式。

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


1

我了解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

-1

现在,我正在考虑template设计模式。使用模块感觉不对。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.