Aquí está la historia completa, explicando los conceptos de metaprogramación necesarios para comprender 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?
La inclusión de 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 a su ancestors
método:
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 buscará en cada elemento de esta lista de ancestros para encontrar un método de instancia con el nombre proporcionado. Dado que incluimos M
en C
, M
ahora es un antepasado de C
, por lo que cuando invocamos foo
una instancia de C
, Ruby encontrará ese método en M
:
C.new.foo
#=> "foo"
Tenga en cuenta que la inclusión no copia ningún método de instancia o clase a la clase ; simplemente agrega una "nota" a la clase de que también debe buscar métodos de instancia en el módulo incluido.
¿Qué pasa con los métodos de "clase" en nuestro módulo?
Debido a que la inclusión solo cambia la forma en que se distribuyen los métodos de instancia, incluir 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 del módulo no se copian automáticamente en 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, tiene 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 singleton oculta especial 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! ¿Eso significa que también pueden tener métodos singleton? ¡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 utilizarlo 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 en realidad solo métodos de instancia en la clase singleton del objeto de clase. ¿Significa esto que podemos 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 self.singleton_class.include M::ClassMethods
línea no se ve muy bien, así 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 extend
llamada al módulo
Este ejemplo anterior no es un código bien estructurado, por dos razones:
- Ahora tenemos que llamar a ambos
include
y extend
en la HostClass
definición para que nuestro módulo se incluya correctamente. Esto puede resultar muy engorroso si tiene que incluir muchos módulos similares.
HostClass
referencias directas M::ClassMethods
, que es un detalle de implementación del módulo M
que HostClass
no debería ser necesario conocer o preocupar.
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 llamarse a extend
sí mismo. De esta manera, es el trabajo del módulo agregar los métodos de clase si así lo desea.
Para eso es exactamente el método especialself.included
. Ruby llama automáticamente a este método siempre que el módulo se incluye en otra clase (o módulo) y pasa el objeto de la 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 self.included
. Tenemos el objeto de clase, por lo que podemos llamar a cualquier otro método (clase) sobre él:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end