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