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

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

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

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

7 ответов

Решение

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


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

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

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

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

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

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

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

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

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

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

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

И да, ActiveRecord должен был быть включен, а не расширен подклассом. Другой ORM - datamapper - точно достигает этого!

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

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

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

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

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

class Parent
end 

module M
end

class Child < Parent
  include M
end

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

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

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

Другие вопросы по тегам