define_method - modules in ruby
herencia ruby vs mixins (7)
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?
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 compilador no te advierte en absoluto cuando ocurren las colisiones.
Yo sostengo que este comportamiento no se escala a grandes equipos de programadores. No debe suponerse que la persona que implementa la class C
conoce todos los nombres en el alcance. 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.
En este momento, estoy pensando en el template
diseño de la template
. Simplemente no se sentiría bien con un módulo.
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
=> [#<Class:Test>, M, #<Class:Object>, ...
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 objetivo (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
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.
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.
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.