operador modulos herencia clases ruby mixins

modulos - Heredar métodos de clase de módulos/mixins en Ruby



modulos ruby (4)

Aquí está la historia completa, explicando los conceptos de metaprogramación necesarios para entender por qué la inclusión de módulos funciona de la manera en que lo hace en Ruby.

¿Qué sucede cuando se incluye un módulo?

Incluir un módulo en una clase agrega el módulo a los antepasados de la clase. Puede ver los antepasados ​​de cualquier clase o módulo llamando al método de sus ancestors :

module M def foo; "foo"; end end class C include M def bar; "bar"; end end C.ancestors #=> [C, M, Object, Kernel, BasicObject] # ^ look, it''s right here!

Cuando llamas a un método en una instancia de C , Ruby observará cada elemento de esta lista de ancestros para encontrar un método de instancia con el nombre provisto. Dado que incluimos M en C , M ahora es un ancestro de C , por lo que cuando llamamos a foo en una instancia de C , Ruby encontrará ese método en M :

C.new.foo #=> "foo"

Tenga en cuenta que la inclusión no copia ninguna instancia o método de clase a la clase ; simplemente agrega una "nota" a la clase que también debe buscar los métodos de la instancia en el módulo incluido.

¿Qué pasa con los métodos de "clase" en nuestro módulo?

Porque la inclusión solo cambia la forma en que se distribuyen los métodos de instancia, incluido un módulo en una clase, solo hace que sus métodos de instancia estén disponibles en esa clase. Los métodos de "clase" y otras declaraciones en el módulo no se copian automáticamente a la clase:

module M def instance_method "foo" end def self.class_method "bar" end end class C include M end M.class_method #=> "bar" C.new.instance_method #=> "foo" C.class_method #=> NoMethodError: undefined method `class_method'' for C:Class

¿Cómo implementa Ruby los métodos de clase?

En Ruby, las clases y los módulos son objetos simples: son instancias de la clase Class y Module . Esto significa que puede crear dinámicamente nuevas clases, asignarlas a variables, etc.

klass = Class.new do def foo "foo" end end #=> #<Class:0x2b613d0> klass.new.foo #=> "foo"

También en Ruby, tienes la posibilidad de definir los llamados métodos singleton en objetos. Estos métodos se agregan como nuevos métodos de instancia a la clase especial especial oculta del objeto:

obj = Object.new # define singleton method def obj.foo "foo" end # here is our singleton method, on the singleton class of `obj`: obj.singleton_class.instance_methods(false) #=> [:foo]

Pero, ¿no son las clases y los módulos simplemente objetos simples también? De hecho, lo son! ¿Significa eso que también pueden tener métodos únicos? Sí, lo hace! Y así es como nacen los métodos de clase:

class Abc end # define singleton method def Abc.foo "foo" end Abc.singleton_class.instance_methods(false) #=> [:foo]

O bien, la forma más común de definir un método de clase es usar self dentro del bloque de definición de clase, que se refiere al objeto de clase que se está creando:

class Abc def self.foo "foo" end end Abc.singleton_class.instance_methods(false) #=> [:foo]

¿Cómo incluyo los métodos de clase en un módulo?

Como acabamos de establecer, los métodos de clase son realmente solo métodos de instancia en la clase singleton del objeto de clase. ¿Esto significa que podemos simplemente incluir un módulo en la clase singleton para agregar un montón de métodos de clase? Sí, lo hace!

module M def new_instance_method; "hi"; end module ClassMethods def new_class_method; "hello"; end end end class HostKlass include M self.singleton_class.include M::ClassMethods end HostKlass.new_class_method #=> "hello"

Esta línea self.singleton_class.include M::ClassMethods no se ve muy bien, por lo que Ruby agregó Object#extend , que hace lo mismo, es decir, incluye un módulo en la clase singleton del objeto:

class HostKlass include M extend M::ClassMethods end HostKlass.singleton_class.included_modules #=> [M::ClassMethods, Kernel] # ^ there it is!

Mover la llamada de extend al módulo

Este ejemplo anterior no es un código bien estructurado, por dos razones:

  1. Ahora tenemos que llamar tanto para include como para extend en la definición de HostClass para que nuestro módulo se incluya correctamente. Esto puede ser muy engorroso si tiene que incluir muchos módulos similares.
  2. HostClass referencia directamente a M::ClassMethods , que es un detalle de la implementación del módulo M que HostClass no debería necesitar saber o preocuparse.

Entonces, ¿qué tal esto?: Cuando llamamos include en la primera línea, de alguna manera notificamos al módulo que se ha incluido, y también le damos nuestro objeto de clase, para que pueda llamar a extend . De esta forma, es tarea del módulo agregar los métodos de clase si así lo desea.

Esto es exactamente para lo que es el método especial del self.included . Ruby llama automáticamente a este método cada vez que el módulo se incluye en otra clase (o módulo), y pasa el objeto de clase de host como primer argumento:

module M def new_instance_method; "hi"; end def self.included(base) # `base` is `HostClass` in our case base.extend ClassMethods end module ClassMethods def new_class_method; "hello"; end end end class HostKlass include M def self.existing_class_method; "cool"; end end HostKlass.singleton_class.included_modules #=> [M::ClassMethods, Kernel] # ^ still there!

Por supuesto, agregar métodos de clase no es lo único que podemos hacer en self.included . self.included . Tenemos el objeto de clase, por lo que podemos llamar a cualquier otro método (clase) en él:

def self.included(base) # `base` is `HostClass` in our case base.existing_class_method #=> "cool" end

Se sabe que en Ruby, los métodos de clase se heredan:

class P def self.mm; puts ''abc'' end end class Q < P; end Q.mm # works

Sin embargo, es una sorpresa para mí que no funcione con mixins:

module M def self.mm; puts ''mixin'' end end class N; include M end M.mm # works N.mm # does not work!

Sé que el método #extend puede hacer esto:

module X; def mm; puts ''extender'' end end Y = Class.new.extend X X.mm # works

Pero estoy escribiendo un mixin (o, más bien, me gustaría escribir) que contiene métodos de instancia y métodos de clase:

module Common def self.class_method; puts "class method here" end def instance_method; puts "instance method here" end end

Ahora lo que me gustaría hacer es esto:

class A; include Common # custom part for A end class B; include Common # custom part for B end

Deseo que A, B herede los métodos de instancia y clase del módulo Common . Pero, por supuesto, eso no funciona. Entonces, ¿no hay una forma secreta de hacer que esta herencia funcione desde un único módulo?

Me parece poco elegante dividir esto en dos módulos diferentes, uno para incluir y el otro para extender. Otra posible solución sería usar una clase Common lugar de un módulo. Pero esto es solo una solución. (¿Qué pasa si hay dos conjuntos de funcionalidades comunes Common1 y Common2 y realmente necesitamos tener mixins?) ¿Hay alguna razón profunda por la cual la herencia del método de clase no funciona desde mixins?


Como mencionó Sergio en los comentarios, para los chicos que ya están en Rails (o no les importa depender de Soporte Activo ), la Concern es útil aquí:

require ''active_support/concern'' module Common extend ActiveSupport::Concern def instance_method puts "instance method here" end class_methods do def class_method puts "class method here" end end end class A include Common end


Puedes tener tu pastel y comértelo también al hacer esto:

module M def self.included(base) base.class_eval do # do anything you would do at class level def self.doit #class method @@fred = "Flintstone" "class method doit called" end # class method define def doit(str) #instance method @@common_var = "all instances" @instance_var = str "instance method doit called" end def get_them [@@common_var,@instance_var,@@fred] end end # class_eval end # included end # module class F; end F.include M F.doit # >> "class method doit called" a = F.new b = F.new a.doit("Yo") # "instance method doit called" b.doit("Ho") # "instance method doit called" a.get_them # >> ["all instances", "Yo", "Flintstone"] b.get_them # >> ["all instances", "Ho", "Flintstone"]

Si tiene la intención de agregar una instancia y variables de clase, terminará sacándose el pelo, ya que se encontrará con un montón de código roto a menos que lo haga de esta manera.


Una expresión común es usar hook included e inyectar métodos de clase desde allí.

module Foo def self.included base base.send :include, InstanceMethods base.extend ClassMethods end module InstanceMethods def bar1 ''bar1'' end end module ClassMethods def bar2 ''bar2'' end end end class Test include Foo end Test.new.bar1 # => "bar1" Test.bar2 # => "bar2"