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

127

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

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

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

Брэд Купит
источник

Ответы:

176

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


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

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

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

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

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

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

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

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

Энди Гаскелл
источник
1
Пример наглядно показывает: Truck IS A Vehicle - нет грузовика, который не был бы транспортным средством.
PL J
1
Пример четко показывает это - TruckIS A Vehicle- нет Truckтого, что не было бы Vehicle. Однако я бы назвал модуль возможно SelfPropelable(:?) Хм SelfPropeledзвучит правильно, но это почти то же самое: D. Во всяком случае, я бы не стал включать его, Vehicleно ... Truckпоскольку ЕСТЬ Транспортные средства, которых НЕТ SelfPropeled. Также хороший признак - спросить - А есть ли другие вещи, а НЕ автомобили SelfPropeled? - Ну, может быть, но найти будет труднее. Таким образом, Vehicleможно наследовать от класса SelfPropelling (как класс он не подходит для SelfPropeled- поскольку это скорее роль)
PL J
39

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

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 «s константы и методы в неожиданных пространствах имен. Проблема в том, что компилятор вообще не предупреждает вас о столкновениях.

Я утверждаю, что такое поведение не масштабируется для больших команд программистов - не следует предполагать, что реализующий человек class Cзнает обо всех именах в области видимости. Ruby даже позволит вам переопределить константу или метод другого типа . Я не уверен , что может когда - либо считать правильное поведение.

Дэн Барови
источник
2
Это мудрое предостережение. Напоминает подводные камни множественного наследования в C ++.
Крис Тонкинсон
1
Есть ли хорошее смягчение этого? Это похоже на причину, по которой множественное наследование Python является превосходным решением (не пытаться начать сопоставление языка; просто сравнивая эту конкретную функцию).
Марчин
1
@bazz Отлично и все такое, но композиция на большинстве языков громоздка. Это также наиболее актуально для языков с утиным типом. Это также не гарантирует, что вы не получите странных состояний.
Marcin
Старый пост я знаю, но в поисках все равно оказывается. Ответ частично неверен - C#sayhiвыводит B::HELLOне потому, что Ruby смешивает константы, а потому, что ruby ​​разрешает константы от более близких к отдаленным, поэтому HELLOссылка в Bвсегда будет разрешать B::HELLO. Это справедливо, даже если класс C C::HELLOтоже определил свой собственный .
Лаас
13

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

цыпленок
источник
2
Это дает прямой ответ на вопрос: наследование обеспечивает определенную организационную структуру, которая может сделать ваш проект более читабельным.
emery
10

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

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

nareshb
источник
4

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

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

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

Тило
источник
1

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

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

class Parent
end 

module M
end

class Child < Parent
  include M
end

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

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

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
Донато
источник
-1

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

Geo
источник