¡Bienvenido a un nuevo episodio de Ruby Magic! La edición de este mes trata sobre metaclases, un tema provocado por una discusión entre dos desarrolladores (¡Hola Maud!).
A través del examen de metaclases, aprenderemos cómo funcionan los métodos de clase e instancia en Ruby. En el camino, descubra la diferencia entre definir un método pasando un «definee» explícito y usar class << self
o instance_eval
. ¡Vamos!
Instancias de clase y Métodos de instancia
Para comprender por qué se usan metaclases en Ruby, comenzaremos examinando cuáles son las diferencias entre los métodos de instancia y de clase.
En Ruby, una clase es un objeto que define un blueprint para crear otros objetos. Las clases definen qué métodos están disponibles en cualquier instancia de esa clase.
Definir un método dentro de una clase crea un método de instancia en esa clase. Cualquier instancia futura de esa clase tendrá ese método disponible.
1 2 3 4 5 6 7 8 9 10 11 12 |
class User def initialize(name) @name = name end def name @name end end user = User.new('Thijs') user.name # => "Thijs" |
En este ejemplo, creamos una clase llamada User
, con un método de instancia llamado #name
que devuelve el nombre del usuario. Usando la clase, creamos una instancia de clase y la almacenamos en una variable llamada user
. Dado que user
es una instancia de la clase User
, tiene disponible el método #name
.
Una clase almacena sus métodos de instancia en su tabla de métodos. Cualquier instancia de esa clase se refiere a la tabla de métodos de su clase para obtener acceso a sus métodos de instancia.
Objetos de clase
Un método de clase es un método que se puede llamar directamente en la clase sin tener que crear una instancia primero. Un método de clase se crea prefijando su nombre con self.
al definirlo.
Una clase es en sí misma un objeto. Una constante se refiere al objeto de clase, por lo que los métodos de clase definidos en él se pueden llamar desde cualquier lugar de la aplicación.
1 2 3 4 5 6 7 8 9 |
class User # ... def self.all end end User.all # => |
Los métodos definidos con un prefijo self.
no se agregan a la tabla de métodos de la clase. En su lugar, se agregan a la metaclase de la clase.
Metaclases
Aparte de una clase, cada objeto en Ruby tiene una metaclase oculta. Las metaclases son monoplazas, lo que significa que pertenecen a un solo objeto. Si crea varias instancias de una clase, compartirán la misma clase, pero todas tendrán metaclases separadas.
1 2 3 4 5 6 7 8 9 |
thijs, robert, tom = User.all thijs.class # => User robert.class # => User tom.class # => User thijs.singleton_class # => #<Class:#<User:0x00007fb71a9a2cb0>> robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>> tom.singleton_class # => #<Class:#<User:0x00007fb71a9a2c10>> |
En este ejemplo, vemos que aunque cada uno de los objetos tiene la clase User
, sus clases singleton tienen diferentes ID de objeto, lo que significa que son objetos separados.
Al tener acceso a una metaclase, Ruby permite agregar métodos directamente a objetos existentes. Al hacerlo, no se agregará un método nuevo a la clase del objeto.
1 2 3 4 5 6 7 8 |
robert = User.new("Robert") def robert.last_name "Beekman" end robert.last_name # => "Beekman" User.new("Tom").last_name # => NoMethodError (undefined method `last_name' for #<User:0x00007fe1cb116408>) |
En este ejemplo, agregamos un #last_name
al usuario almacenado en la variable robert
. Aunque robert
es una instancia de User
, cualquier instancia recién creada de User
no tendrá acceso al método #last_name
, ya que solo existe en la metaclase de robert
.
¿Qué es el yo?
Al definir un método y pasar un receptor, el nuevo método se agrega a la metaclase del receptor, en lugar de agregarlo a la tabla de métodos de la clase.
1 2 3 4 5 |
tom = User.new("Tom") def tom.last_name "de Bruijn" end |
En el ejemplo anterior, agregamos #last_name
directamente en el objeto tom
, pasando tom
como receptor al definir el método.
Así es también como funciona para los métodos de clase.
1 2 3 4 5 6 7 |
class User # ... def self.all end end |
Aquí, pasamos explícitamente self
como receptor al crear el método .all
. En una definición de clase, self
se refiere a la clase (User
en este caso), por lo que el método .all
se agrega a la metaclase de User
.
Dado que User
es un objeto almacenado en una constante, accederemos al mismo objeto, y a la misma metaclase, siempre que lo hagamos referencia.
Abriendo la Metaclase
Hemos aprendido que los métodos de clase son métodos en la metaclase del objeto de clase. Sabiendo esto, veremos algunas otras técnicas de creación de métodos de clase que puede haber visto antes.
class < < self
Aunque ha pasado de moda un poco, algunas bibliotecas usan class << self
para definir métodos de clase. Este truco de sintaxis abre la metaclase de la clase actual e interactúa con ella directamente.
1 2 3 4 5 6 7 8 9 10 11 |
class User class << self self # => #<Class:User> def all end end end User.all # => |
Este ejemplo crea un método de clase llamado User.all
agregando un método a la metaclase de User
. En lugar de pasar explícitamente un receptor para el método como vimos anteriormente, establecemos self
a la metaclase User
en lugar de User
en sí.
Como aprendimos antes, cualquier definición de método sin un receptor explícito se agrega como método de instancia de la clase actual. Dentro del bloque, la clase actual es la metaclase User
(#<Class:User>
).
instance_eval
Otra opción es usar instance_eval
, que hace lo mismo con una diferencia importante. Aunque la metaclase de la clase recibe los métodos definidos en el bloque, self
sigue siendo una referencia a la clase principal.
1 2 3 4 5 6 7 8 9 10 11 |
class User instance_eval do self # => User def all end end end User.all # => |
En este ejemplo, definimos un método de instancia User
‘s metaclass al igual que antes, pero self
todavía puntos a User
. Aunque normalmente apunta al mismo objeto, el» default definee » y self
pueden apuntar a diferentes objetos.
Lo que hemos aprendido
Hemos aprendido que las clases son los únicos objetos que pueden tener métodos, y que los métodos de instancia son en realidad métodos en la metaclase de un objeto. Sabemos que class << self
simplemente intercambia self
para permitirle definir métodos en la metaclase, y sabemos que instance_eval
hace casi lo mismo (pero sin tocar self
).
Aunque no trabajarás explícitamente con metaclases, Ruby las usa extensamente bajo el capó. Saber qué sucede cuando defines un método puede ayudarte a entender por qué Ruby se comporta como lo hace (y por qué tienes que anteponer los métodos de clase con self.
).
Gracias por leer. Si le gustó lo que leyó, puede suscribirse a Ruby Magic para recibir un correo electrónico cuando publiquemos un nuevo artículo aproximadamente una vez al mes.