рубиновое наследование против миксинов


в Ruby, поскольку вы можете включить несколько миксинов, но только расширить один класс, похоже, что миксины будут предпочтительнее наследования.

мой вопрос: если вы пишете код, который должен быть расширен/включен, чтобы быть полезным, почему вы когда-нибудь сделаете его классом? Или, другими словами, почему бы вам не сделать его всегда модулем?

Я могу думать только об одной причине, почему вы хотите класс, а вот если вам нужно создать экземпляр класса. В случае ActiveRecord:: Base, однако вы никогда не создаете его напрямую. Так разве это не должен был быть модуль вместо этого?

7 121

7 ответов:

Я просто читайте об этой теме в Обоснованный Рубист (отличная книга, кстати). Автор делает лучшую работу по объяснению, чем я, поэтому я процитирую его:


ни одно правило или формула не всегда приводит к правильному дизайну. Но это полезно, чтобы сохранить несколько соображений в виду, когда вы делаете решения класса против модуля:

  • модули не имеют экземпляров. Из этого следует, что сущности или вещи, как правило, лучше всего моделируется в классах, а характеристики или свойства сущностей или вещей являются лучше всего инкапсулировать в модули. Соответственно, как указано в разделе 4.1.1, класс имена, как правило, существительные, в то время как имена модулей часто прилагательные (стек против Stacklike).

  • класс может иметь только один суперкласс, но он может смешивать столько модулей, сколько захочет. если вы используете наследование, отдаете приоритет создание разумного суперкласса / подкласса отношение. Не используйте одно и только суперкласс отношения класса к наделите класс тем, что может оказаться всего лишь одним из нескольких наборов характеристик.

суммируя эти правила в одном примере, вот что вы не должны делать:

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

скорее, вы должны сделать это:

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

вторая версия моделирует объекты и свойства гораздо более аккуратно. Грузовик спускается от Автомобиль (что имеет смысл), в то время как самопроизвольное движение-это характеристика транспортных средств (по крайней мере, всех тех, о которых мы заботимся в этой модели мира) - характеристика, которая передается грузовикам в силу того, что грузовик является потомком или специализированным форма, транспортного средства.

Я думаю, что миксины-отличная идея, но здесь есть еще одна проблема, о которой никто не упоминал: столкновения пространств имен. Рассмотрим:

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 даже позволит вам переопределить константу или метод a другой тип. Я не уверен, что это может когда-нибудь считается правильным поведением.

мое взятие: модули предназначены для совместного использования поведения, в то время как классы предназначены для моделирования отношений между объектами. Вы технически можете просто сделать все экземпляром объекта и смешать в любых модулях, которые вы хотите получить желаемый набор поведений, но это будет плохой, бессистемный и довольно нечитаемый дизайн.

ответ на ваш вопрос сильно зависит от контекста. По наблюдениям pubb, выбор в первую очередь обусловлен рассматриваемым доменом.

и да, ActiveRecord должен был быть включен, а не расширен подклассом. Еще один ОРМ -datamapper - именно этого и добивается!

Мне очень нравится ответ Энди Гаскелла-просто хотел добавить, что да, ActiveRecord не должен использовать наследование, а скорее включать модуль для добавления поведения (в основном персистентности) к модели/классу. ActiveRecord просто использует неправильную парадигму.

по той же причине мне очень нравится MongoId над MongoMapper, потому что он оставляет разработчику возможность использовать наследование как способ моделирования чего-то значимого в проблемной области.

грустно, что почти никто в сообществе Rails не использует "Рубиновое наследование" так, как оно должно использоваться - для определения иерархий классов, а не только для добавления поведения.

лучший способ понять Миксины - это виртуальные классы. Миксины-это "виртуальные классы", которые были введены в цепочку предков класса или модуля.

когда мы используем "include" и передаем ему модуль, он добавляет модуль в цепочку предков прямо перед классом, который мы наследуем от:

class Parent
end 

module M
end

class Child < Parent
  include M
end

Child.ancestors
 => [Child, M, Parent, Object ...

каждый объект в Ruby также имеет одноэлементный класс. Методы, добавленные к этому одноэлементному классу, могут быть непосредственно вызваны на объект и поэтому они действуют как " класс" методы. Когда мы используем "extend" на объекте и передаем объекту модуль, мы добавляем методы модуля в одноэлементный класс объекта:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

мы можем получить доступ к классу singleton с помощью метода singleton_class:

Test.singleton_class.ancestors
 => [#<Class:Test>, M, #<Class:Object>, ...

Ruby предоставляет некоторые крючки для модулей, когда они смешиваются в классы/модули. included - это метод hook, предоставляемый 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 шаблон дизайна. Это просто не будет чувствовать себя хорошо с модулем.