【问题标题】:ruby inheritance vs mixins红宝石继承与混合
【发布时间】:2010-11-19 22:50:14
【问题描述】:

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

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

我只能想到你想要一个类的一个原因,那就是如果你需要实例化这个类。然而,在 ActiveRecord::Base 的情况下,您永远不会直接实例化它。那么它不应该是一个模块吗?

【问题讨论】:

    标签: ruby class inheritance module mixins


    【解决方案1】:

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

    【讨论】:

    • 您能否进一步说明您的陈述并为问题添加正确答案?
    【解决方案2】:

    刚刚The Well-Grounded Rubyist 中读到了这个主题(顺便说一句,这本书很棒)。作者的解释比我做得更好,所以我会引用他的话:


    没有单一的规则或公式总能产生正确的设计。但是保留一个很有用 当你做出类与模块的决定时,要考虑几个因素:

    • 模块没有实例。 因此,实体或事物通常是最好的 以类为模型,实体或事物的特征或属性是 最好封装在模块中。相应地,如第 4.1.1 节所述,类 名称往往是名词,而模块名称通常是形容词(堆栈 与 Stacklike 相比)。

    • 一个类只能有一个超类,但它可以混合任意数量的模块。 如果 你正在使用继承,优先创建一个合理的超类/子类 关系。不要用完一个类的唯一超类关系 赋予该类可能只是几组特征之一。

    在一个例子中总结这些规则,以下是你不应该做的事情:

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

    相反,你应该这样做:

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

    第二个版本更简洁地对实体和属性进行建模。卡车 来自 Vehicle (这是有道理的),而 SelfPropelling 是车辆的一个特征(至少,我们在这个世界模型中关心的所有那些)——由于 Truck 是后代而传递给卡车的特征,或者专门 形式,车辆。

    【讨论】:

    • 这个例子巧妙地展示了它 - 卡车是车辆 - 没有卡车不会是车辆。
    • 这个例子很好地展示了它 - Truck IS A Vehicle - 没有 Truck 不会是 Vehicle。但是我可能会调用模块 SelfPropelable (:?) hmm SelfPropeled 听起来不错,但几乎相同:D。无论如何,我不会将其包含在Vehicle 中,而是包含在Truck 中——因为有些车辆不是SelfPropeled。还有一个很好的指示是问 - 还有其他东西,而不是 SelfPropeled 的车辆? - 好吧也许,但我会更难找到。所以Vehicle 可以从类 SelfPropelling 继承(因为它不适合 SelfPropeled 的类——因为这更像是一个角色)
    • 基类继承和包含模块之间存在细微差别:gist.github.com/yoelblum/12b6a648d0ff7df4e3c0e95a55e8be1d
    【解决方案3】:

    我的看法:模块用于共享行为,而类用于建模对象之间的关系。从技术上讲,您可以将所有内容都设为 Object 的实例,然后混入您想要获得所需行为集的任何模块,但这将是一种糟糕、随意且难以理解的设计。

    【讨论】:

    • 这直接回答了这个问题:继承强制执行特定的组织结构,可以使您的项目更具可读性。
    【解决方案4】:

    您的问题的答案在很大程度上取决于上下文。提炼 pubb 的观察,选择主要由所考虑的领域驱动。

    是的,ActiveRecord 应该被包含而不是由子类扩展。另一个 ORM - datamapper - 正是实现了这一点!

    【讨论】:

      【解决方案5】:

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

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

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

      【讨论】:

        【解决方案6】:

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

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

        【讨论】:

        • 这是一个明智的警告。让人想起 C++ 中的多重继承陷阱。
        • 有什么好的缓解方法吗?这看起来像是 Python 多重继承是一个优越的解决方案的一个原因(不是试图启动语言 p*ssing 匹配;只是比较这个特定的特性)。
        • @bazz 这很好,但大多数语言的组合很麻烦。它也主要与鸭式语言相关。它也不能保证你不会得到奇怪的状态。
        • 旧帖子,我知道,但仍然在搜索中出现。答案部分不正确 - C#sayhi 输出 B::HELLO 不是因为 Ruby 混淆了常量,而是因为 ruby​​ 解析常量从近到远 - 所以在 B 中引用的 HELLO 将始终解析为 B::HELLO。即使 C 类也定义了它自己的 C::HELLO,这仍然成立。
        【解决方案7】:

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

        【讨论】:

          猜你喜欢
          • 2012-08-30
          • 1970-01-01
          • 1970-01-01
          • 2019-08-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-09-09
          • 1970-01-01
          相关资源
          最近更新 更多