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

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

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

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


person Brad Cupit    schedule 15.08.2009    source источник


Ответы (7)


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


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

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

person Andy Gaskell    schedule 15.08.2009
comment
Пример наглядно демонстрирует это - Truck IS A Vehicle - нет грузовика, который не был бы транспортным средством. - person PL J; 24.01.2016
comment
Пример четко показывает - Truck IS A Vehicle - нет Truck, который не был бы Vehicle. Однако я бы назвал модуль, возможно, SelfPropelable (:?) Хмм SelfPropeled звучит правильно, но это почти то же самое: D. Во всяком случае, я бы не стал включать его в Vehicle, но в Truck - поскольку ЕСТЬ Транспортные средства, которых НЕ SelfPropeled. Также хороший признак - спросить - есть ли другие вещи, НЕ ЕСТЬ Транспортные средства, которые ЯВЛЯЮТСЯ SelfPropeled? - Ну, может быть, но найти будет труднее. Итак, Vehicle может унаследовать от класса SelfPropelling (как класс он не подходит как SelfPropeled - это скорее роль) - person PL J; 24.01.2016
comment
Есть тонкая разница между наследованием базового класса и включением модулей: gist.github.com/yoelblum/12b8edd1 > - person Joel Blum; 29.08.2020

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

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

person Dan Barowy    schedule 26.04.2012
comment
Это мудрое предостережение. Напоминает подводные камни множественного наследования в C ++. - person Chris Tonkinson; 17.01.2014
comment
Есть ли хорошее смягчение этого? Это похоже на причину, по которой множественное наследование Python является превосходным решением (не пытаться начать сопоставление языка; просто сравнивая эту конкретную функцию). - person Marcin; 17.04.2015
comment
@bazz Это здорово и все такое, но композиция на большинстве языков громоздка. Это также в основном актуально для языков с утиным типом. Это также не гарантирует, что вы не получите странных состояний. - person Marcin; 01.05.2016
comment
Старый пост, я знаю, но все равно в поисках оказывается. Ответ частично неверен - C#sayhi выводит B::HELLO не потому, что Ruby смешивает константы, а потому, что ruby ​​разрешает константы от более близких к удаленным - поэтому HELLO, на который ссылается B, всегда будет преобразовываться в B::HELLO. Это справедливо, даже если класс C тоже определил свой C::HELLO. - person Laas; 09.11.2017

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

person Chuck    schedule 15.08.2009
comment
Это дает прямой ответ на вопрос: наследование обеспечивает определенную организационную структуру, которая может сделать ваш проект более читабельным. - person emery; 07.04.2016

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

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

person nareshb    schedule 16.08.2009

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

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

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

person Tilo    schedule 04.04.2011

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

Когда мы используем "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
person Donato    schedule 06.05.2018

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

person Geo    schedule 15.08.2009