Desentrañar Clases, Instancias y Metaclases en Ruby

¡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 Useren 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.



+