рубиновое наследование против миксинов
в Ruby, поскольку вы можете включить несколько миксинов, но только расширить один класс, похоже, что миксины будут предпочтительнее наследования.
мой вопрос: если вы пишете код, который должен быть расширен/включен, чтобы быть полезным, почему вы когда-нибудь сделаете его классом? Или, другими словами, почему бы вам не сделать его всегда модулем?
Я могу думать только об одной причине, почему вы хотите класс, а вот если вам нужно создать экземпляр класса. В случае ActiveRecord:: Base, однако вы никогда не создаете его напрямую. Так разве это не должен был быть модуль вместо этого?
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