herencia ruby ​​vs mixins

En Ruby, dado que puedes incluir múltiples mixins pero solo extender una clase, parece que las mixins serían preferibles a la herencia.

Mi pregunta: si está escribiendo código que debe ser extendido / incluido para ser útil, ¿por qué alguna vez lo convertiría en una clase? O dicho de otra manera, ¿por qué no siempre lo convertirías en un módulo?

Solo puedo pensar en una razón por la que querrías una clase, y eso es si necesitas instanciar la clase. En el caso de ActiveRecord :: Base, sin embargo, nunca se crea una instancia directamente. Entonces, ¿no debería haber sido un módulo?

Acabo de leer sobre este tema en The Rubyist Well-Grounded (gran libro, por cierto). El autor hace un mejor trabajo de explicación que yo, así que lo citaré:


Ninguna regla o fórmula individual siempre da como resultado el diseño correcto. Pero es útil tener en cuenta un par de consideraciones cuando se toman decisiones de clase versus módulo:

  • Los módulos no tienen instancias. Se deduce que las entidades o cosas generalmente se modelan mejor en clases, y las características o propiedades de las entidades o cosas se encapsulan mejor en los módulos. En consecuencia, como se señala en la sección 4.1.1, los nombres de las clases tienden a ser sustantivos, mientras que los nombres de los módulos suelen ser adjetivos (Stack versus Stacklike).

  • Una clase solo puede tener una superclase, pero puede combinar tantos módulos como desee. Si usa herencia, dé prioridad a la creación de una relación de superclase / subclase sensible. No use la única y única relación de superclase de una clase para dotar a la clase con lo que podría ser solo uno de varios conjuntos de características.

Resumiendo estas reglas en un ejemplo, esto es lo que no debes hacer:

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

Por el contrario, deberías hacer esto:

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

La segunda versión modela las entidades y propiedades de forma mucho más clara. El camión desciende de Vehicle (lo cual tiene sentido), mientras que SelfPropelling es una característica de los vehículos (al menos, todos los que nos importan en este modelo del mundo), una característica que se transmite a los camiones en virtud de que Truck es un descendiente. o forma especializada, de Vehículo.

Creo que los mixins son una gran idea, pero aquí hay otro problema que nadie ha mencionado: las colisiones del espacio de nombres. Considerar:

 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 

¿Cuál gana? En Ruby, resulta ser el último, module B , porque lo incluiste después del module A Ahora, es fácil evitar este problema: asegúrese de que todas las constantes y métodos del module A y del module B encuentren en espacios de nombres poco probables. El problema es que el comstackdor no te advierte en absoluto cuando ocurren las colisiones.

Yo sostengo que este comportamiento no se escala a grandes equipos de progtwigdores. No debe suponerse que la persona que implementa la class C conoce todos los nombres en el scope. Ruby incluso te permitirá anular una constante o método de un tipo diferente . No estoy seguro de que alguna vez se pueda considerar un comportamiento correcto.

Mi opinión: los módulos son para compartir el comportamiento, mientras que las clases son para modelar las relaciones entre los objetos. Técnicamente podrías simplemente hacer de todo un ejemplo de Object y mezclar los módulos en los que desees obtener el conjunto deseado de comportamientos, pero ese sería un diseño pobre, aleatorio y bastante ilegible.

La respuesta a tu pregunta es en gran medida contextual. Según la observación de Bremen, la elección depende principalmente del dominio en cuestión.

Y sí, ActiveRecord debería haber sido incluido en lugar de extendido por una subclase. Otro ORM – DataMapper : ¡eso lo logra con precisión!

Me gusta mucho la respuesta de Andy Gaskell: solo quería agregar que sí, ActiveRecord no debería usar herencia, sino más bien incluir un módulo para agregar el comportamiento (principalmente la persistencia) a un modelo / clase. ActiveRecord simplemente está usando el paradigma equivocado.

Por la misma razón, me gusta mucho MongoId sobre MongoMapper, porque deja al desarrollador la oportunidad de usar la herencia como una forma de modelar algo significativo en el dominio del problema.

Es triste que prácticamente nadie en la comunidad de Rails esté usando la “herencia de Ruby” de la manera en que se supone que debe usarse: para definir jerarquías de clases, no solo para agregar comportamientos.

La mejor manera en que entiendo mixins son como clases virtuales. Mixins son “clases virtuales” que se han inyectado en la cadena de antepasados ​​de una clase o módulo.

Cuando usamos “incluir” y pasamos un módulo, agrega el módulo a la cadena de ancestros justo antes de la clase de la que estamos heredando:

 class Parent end module M end class Child < Parent include M end Child.ancestors => [Child, M, Parent, Object ... 

Cada objeto en Ruby también tiene una clase singleton. Los métodos agregados a esta clase singleton se pueden invocar directamente sobre el objeto y, por lo tanto, actúan como métodos de “clase”. Cuando usamos “extender” en un objeto y pasamos el objeto a un módulo, estamos agregando los métodos del módulo a la clase singleton del objeto:

 module M def m puts 'm' end end class Test end Test.extend M Test.m 

Podemos acceder a la clase singleton con el método singleton_class:

 Test.singleton_class.ancestors => [#, M, #, ... 

Ruby proporciona algunos ganchos para módulos cuando se mezclan en clases / módulos. included un método de enlace proporcionado por Ruby que se llama cuando se incluye un módulo en algún módulo o clase. Al igual que el incluido, hay un gancho extended asociado para extender. Se llamará cuando un módulo sea extendido por otro módulo o clase.

 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 

Esto crea un patrón interesante que los desarrolladores podrían usar:

 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 

Como puede ver, este único módulo está agregando métodos de instancia, métodos de “clase” y actuando directamente en la clase objective (llamando a a_class_method () en este caso).

ActiveSupport :: Concern encapsula este patrón. Aquí está el mismo módulo reescrito para usar ActiveSupport :: Preocupación:

 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 

En este momento, estoy pensando en el template diseño de la template . Simplemente no se sentiría bien con un módulo.