herencia de rubíes vs mixins

127

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

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

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

Brad Cupit
fuente

Respuestas:

176

Me acaba de leer acerca de este tema en el Rubyist Grounded-Bueno (gran libro, por cierto). El autor explica mejor que yo, así que lo citaré:


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

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

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

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

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

Más bien, debes hacer esto:

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

La segunda versión modela las entidades y propiedades de manera mucho más clara. El camión desciende del vehículo (lo cual tiene sentido), mientras que la autopropulsión es una característica de los vehículos (al menos, todos aquellos 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.

Andy Gaskell
fuente
1
El ejemplo lo muestra claramente: el camión ES un vehículo, no hay un camión que no sea un vehículo.
PL J
1
El ejemplo lo muestra claramente: TruckIS A Vehicle: no hay ninguno Truckque no sea a Vehicle. Sin embargo, llamaría al módulo quizás SelfPropelable(:?) Hmm SelfPropeledsuena bien, pero es casi lo mismo: D. De todos modos, no lo incluiría Vehiclesino en Truck, ya que HAY vehículos que NO SON SelfPropeled. También es una buena indicación preguntar: ¿hay otras cosas, NO vehículos que SON SelfPropeled? - Bueno, tal vez, pero sería más difícil de encontrar. Por Vehiclelo tanto, podría heredar de la clase Autopropulsión (como clase no encajaría como SelfPropeled, ya que es más un papel)
PL J
39

Creo que los mixins son una gran idea, pero hay otro problema aquí que nadie ha mencionado: las colisiones de espacios 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 module A. Ahora, es fácil de evitar este problema: asegurarse de que todos module Ay module B's constantes y métodos están en espacios de nombres improbables. El problema es que el compilador no te advierte en absoluto cuando ocurren colisiones.

Sostengo que este comportamiento no se adapta a grandes equipos de programadores; no debe suponer que la persona que implementa class Cconoce todos los nombres en el alcance. Ruby incluso te permitirá anular una constante o un método de un tipo diferente . No estoy seguro de que alguna vez pueda considerarse un comportamiento correcto.

Dan Barowy
fuente
2
Esta es una sabia palabra de precaución. Con reminiscencias de las múltiples dificultades de herencia en C ++.
Chris Tonkinson
1
¿Hay alguna buena mitigación para esto? Esta parece ser una razón por la cual la herencia múltiple de Python es una solución superior (no intentar iniciar una coincidencia de lenguaje *, solo comparar esta característica específica).
Marcin
1
@bazz Eso es genial y todo, pero la composición en la mayoría de los idiomas es engorrosa. También es principalmente relevante en los idiomas de tipo pato. Tampoco garantiza que no tengas estados extraños.
Marcin
Publicación anterior, lo sé, pero aún resulta en las búsquedas. La respuesta es en parte incorrecta: los C#sayhiresultados B::HELLOno se deben a que Ruby mezcla las constantes, sino porque ruby ​​resuelve las constantes de más cerca a más distantes, por lo que siempre se resuelve la HELLOreferencia en . Esto es válido incluso si la clase C definió que es propia también. BB::HELLOC::HELLO
Laas
13

Mi opinión: los módulos son para compartir comportamientos, mientras que las clases son para modelar relaciones entre objetos. Técnicamente, podría hacer que todo sea una instancia de Object y mezclar los módulos que desee para obtener el conjunto deseado de comportamientos, pero eso sería un diseño pobre, fortuito y bastante ilegible.

Arrojar
fuente
2
Esto responde la pregunta de manera directa: la herencia impone una estructura organizativa específica que puede hacer que su proyecto sea más legible.
esmeril
10

La respuesta a su pregunta es en gran medida contextual. Al destilar la observación de pubb, la elección está impulsada principalmente por el dominio en consideración.

Y sí, ActiveRecord debería haber sido incluido en lugar de extendido por una subclase. ¡Otro ORM - datamapper - logra precisamente eso!

nareshb
fuente
4

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

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

Es triste que casi nadie en la comunidad de Rails esté usando la "herencia de Ruby" de la forma en que se supone que debe usarse: para definir jerarquías de clase, no solo para agregar comportamiento.

Tilo
fuente
1

La mejor forma en que entiendo los mixins es como clases virtuales. Los mixins son "clases virtuales" que se han inyectado en la cadena de ancestros de una clase o módulo.

Cuando usamos "include" y le pasamos un módulo, agrega el módulo a la cadena de antecesores justo antes de la clase de la que heredamos:

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 en el objeto, por lo que 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
 => [#<Class:Test>, M, #<Class:Object>, ...

Ruby proporciona algunos ganchos para los módulos cuando se mezclan en clases / módulos. includedes un método de enlace proporcionado por Ruby que se llama cada vez que incluye un módulo en algún módulo o clase. Al igual que se incluye, hay un extendedgancho 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 módulo único está agregando métodos de instancia, métodos de "clase" y actuando directamente sobre la clase de destino (en este caso, llamando a a_class_method ()).

ActiveSupport :: Preocupación 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
Donato
fuente
-1

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

Geo
fuente