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:
- Ahora tenemos que llamar tanto para
include
como paraextend
en la definición deHostClass
para que nuestro módulo se incluya correctamente. Esto puede ser muy engorroso si tiene que incluir muchos módulos similares. -
HostClass
referencia directamente aM::ClassMethods
, que es un detalle de la implementación del móduloM
queHostClass
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"