¡Maestría en Gestión de Errores! Explorando las Excepciones en Ruby para un Código Robusto
Este tutorial te guiará a través del fascinante mundo de la gestión de errores en Ruby. Aprenderás a implementar mecanismos robustos usando `begin`, `rescue`, `ensure` y `raise`, y cómo construir tus propias excepciones personalizadas para un control total del flujo de tu programa.
📖 Introducción a la Gestión de Errores en Ruby
En el desarrollo de software, los errores son una realidad inevitable. Un programa bien diseñado no es solo aquel que funciona correctamente bajo condiciones ideales, sino también aquel que maneja con gracia y eficacia las situaciones inesperadas. En Ruby, las excepciones son el mecanismo principal para gestionar estos errores, permitiéndonos interceptar problemas, tomar acciones correctivas y mantener la estabilidad de nuestra aplicación.
Ignorar la gestión de errores puede llevar a caídas inesperadas de programas, pérdida de datos o una experiencia de usuario frustrante. Dominar cómo "atrapar" y "manejar" excepciones es una habilidad fundamental para cualquier desarrollador Ruby.
En este tutorial, exploraremos los fundamentos de las excepciones en Ruby, desde los bloques básicos begin/rescue/ensure hasta la creación de nuestras propias jerarquías de excepciones personalizadas. Prepárate para escribir código más robusto, predecible y fácil de depurar.
🚨 ¿Qué son las Excepciones y por qué son Importantes?
Una excepción es un evento que ocurre durante la ejecución de un programa y que interrumpe el flujo normal de las instrucciones. En lugar de un fallo abrupto, Ruby "lanza" una excepción, que luego puede ser "capturada" y "manejada" por otra parte del código.
Tipos Comunes de Excepciones en Ruby
Ruby tiene una jerarquía rica de clases de excepciones. Todas las clases de excepción descienden de Exception. Las excepciones que normalmente manejamos descienden de StandardError, que a su vez desciende de Exception.
Aquí tienes algunos ejemplos comunes:
NoMethodError: Se invoca un método que no existe en un objeto.ArgumentError: Se pasan argumentos incorrectos a un método.NameError: Se hace referencia a una variable o método no definido.TypeError: Una operación se intenta en un objeto de tipo incorrecto.ZeroDivisionError: Se intenta dividir por cero.IOError: Problemas relacionados con operaciones de entrada/salida.Errno::ENOENT: Archivo o directorio no encontrado.
¿Por qué `StandardError` y no `Exception` directamente?
Por convención, los bloques `rescue` sin un tipo de excepción especificado capturan solo `StandardError` y sus subclases. Capturar `Exception` directamente puede atrapar señales del sistema como `SystemExit` o `Interrupt`, que normalmente no queremos manejar, ya que impiden que el programa termine correctamente o responda a interrupciones. Es una buena práctica casi siempre rescatar `StandardError` o sus subclases más específicas.El Valor de una Buena Gestión de Errores
- Estabilidad del Programa: Previene caídas inesperadas, manteniendo la aplicación en funcionamiento.
- Experiencia de Usuario: Proporciona mensajes de error amigables en lugar de crípticos "stack traces".
- Recuperación: Permite que el programa intente recuperarse de un problema (por ejemplo, reintentar una operación, usar un valor por defecto).
- Depuración: Ayuda a identificar y localizar problemas rápidamente al registrar información relevante.
- Seguridad: Evita exponer detalles internos sensibles del sistema a usuarios malintencionados a través de mensajes de error.
🛠️ begin, rescue, else, ensure y retry: Los Pilares de la Gestión de Errores
Ruby proporciona una estructura elegante para manejar excepciones utilizando estas palabras clave. Vamos a explorarlas en detalle.
begin y rescue (Intentar y Rescatar)
El bloque begin marca la sección de código donde pueden ocurrir excepciones. El bloque rescue define el código que se ejecutará si se lanza una excepción dentro del bloque begin.
def dividir(a, b)
begin
resultado = a / b
puts "El resultado de la división es: #{resultado}"
rescue ZeroDivisionError => e
puts "⚠️ ¡Error! No se puede dividir por cero. Mensaje: #{e.message}"
rescue TypeError => e
puts "⚠️ ¡Error! Tipos de datos incorrectos para la división. Mensaje: #{e.message}"
rescue StandardError => e # Captura cualquier otra excepción estándar
puts "⚠️ ¡Ocurrió un error inesperado! Tipo: #{e.class}, Mensaje: #{e.message}"
end
end
dividir(10, 2) # Salida: El resultado de la división es: 5
dividir(10, 0) # Salida: ⚠️ ¡Error! No se puede dividir por cero.
dividir('a', 2) # Salida: ⚠️ ¡Error! Tipos de datos incorrectos para la división.
dividir(nil, 2) # Salida: ⚠️ ¡Ocurrió un error inesperado! Tipo: NoMethodError
def leer_archivo(nombre_archivo)
archivo = File.open(nombre_archivo, 'r')
contenido = archivo.read
puts "Contenido del archivo: #{contenido}"
rescue Errno::ENOENT => e
puts "⚠️ ¡Error! El archivo '#{nombre_archivo}' no se encontró. Mensaje: #{e.message}"
rescue IOError => e
puts "⚠️ ¡Error de I/O al leer el archivo. Mensaje: #{e.message}"
ensure
archivo.close if archivo # Asegura que el archivo se cierre siempre
end
leer_archivo('archivo_existente.txt') # Asumiendo que existe
leer_archivo('archivo_inexistente.txt')
Accediendo a la Excepción (=> e)
El => e después del tipo de excepción es opcional, pero muy útil. Asigna el objeto de la excepción a la variable e, lo que te permite acceder a sus propiedades, como e.message (el mensaje de error) y e.backtrace (la traza de la pila, útil para depuración).
def procesar_dato(dato)
raise ArgumentError, 'El dato no puede ser nulo' if dato.nil?
# ... procesamiento
rescue ArgumentError => mi_error
puts "Error en el procesamiento: #{mi_error.message}"
puts "Detalles (backtrace):\n#{mi_error.backtrace.join("\n")}"
end
procesar_dato(nil)
else (Cuando no hay excepción)
El bloque else se ejecuta solo si no se lanzó ninguna excepción dentro del bloque begin. Es útil para separar el código que solo debe ejecutarse si todo salió bien.
def realizar_operacion_segura
begin
# Código que podría lanzar una excepción
puts "Intentando una operación segura..."
# 1 / 0 # Descomenta para ver la excepción
puts "Operación completada sin excepciones."
rescue ZeroDivisionError => e
puts "Error: #{e.message}"
else
puts "✅ ¡Todo salió bien! El bloque 'else' se ejecutó."
ensure
puts "El bloque 'ensure' siempre se ejecuta."
end
end
realizar_operacion_segura()
puts "---"
# 1 / 0 # Esto causaría una ZeroDivisionError si no estuviera comentado
realizar_operacion_segura()
ensure (Siempre)
El bloque ensure es crucial. El código dentro de ensure siempre se ejecuta, independientemente de si se lanzó una excepción o no, o si se manejó. Es perfecto para tareas de limpieza, como cerrar archivos, conexiones de base de datos o liberar recursos.
def operar_con_recurso
recurso = nil
begin
recurso = open('mi_archivo.txt', 'w') # Simula abrir un recurso
recurso.puts "Escribiendo datos..."
# raise 'Simulando un error inesperado'
rescue StandardError => e
puts "Se capturó un error: #{e.message}"
ensure
if recurso
recurso.close
puts "Recurso cerrado en el bloque ensure."
end
end
end
op_with_resource()
retry (Reintentar)
La palabra clave retry se utiliza dentro de un bloque rescue para volver a ejecutar el bloque begin desde el principio. Es útil para operaciones que pueden fallar temporalmente, como conexiones de red o acceso a recursos bloqueados.
⚠️ Advertencia: Usa retry con precaución. Un bucle retry sin una condición de salida puede llevar a un bucle infinito si la causa de la excepción no se resuelve.
intentos = 0
MAX_INTENTOS = 3
def conectar_a_servicio
begin
intentos += 1
puts "Intentando conectar al servicio (Intento #{intentos})..."
raise "Fallo de conexión" if intentos < MAX_INTENTOS
puts "Conexión exitosa al servicio!"
rescue => e
puts "Error: #{e.message}"
if intentos < MAX_INTENTOS
puts "Reintentando en 1 segundo..."
sleep 1
retry # Vuelve a ejecutar el 'begin'
else
puts "Fallo persistente después de #{MAX_INTENTOS} intentos. Abortando."
raise # Re-lanza la última excepción si falla definitivamente
end
end
end
conectar_a_servicio()
💥 Lanzando Excepciones con raise
raise es la herramienta que usamos para generar explícitamente una excepción. Puedes lanzar una instancia de una clase de excepción o, de forma más sencilla, pasar un mensaje.
Sintaxis de raise
raise(sin argumentos): Re-lanza la excepción actualmente activa (solo dentro de un bloquerescue).raise "Mensaje de error": Lanza unaRuntimeErrorcon el mensaje especificado.raise ClaseDeExcepcion: Lanza una nueva instancia de la clase de excepción.raise ClaseDeExcepcion, "Mensaje de error": Lanza una instancia de la clase con el mensaje.raise instancia_de_excepcion: Lanza una instancia de excepción ya creada.
def validar_edad(edad)
raise ArgumentError, "La edad no puede ser negativa" if edad < 0
raise ArgumentError, "La edad debe ser un número entero" unless edad.is_a? Integer
puts "Edad validada: #{edad}"
end
begin
validar_edad(30)
validar_edad(-5)
rescue ArgumentError => e
puts "Error de validación: #{e.message}"
end
begin
validar_edad(25.5)
rescue ArgumentError => e
puts "Error de validación: #{e.message}"
end
begin
# Re-lanzar una excepción dentro de rescue
raise "Primer error"
rescue => e
puts "Capturado: #{e.message}"
puts "Re-lanzando..."
raise # Re-lanza la excepción actual
end
¿Cuándo usar raise?
- Validación de Argumentos: Cuando un método recibe datos no válidos que impiden su funcionamiento.
- Condiciones Anormales: Cuando el programa llega a un estado que no debería ser posible.
- Delegación de Errores: Cuando una capa de tu aplicación detecta un error pero no sabe cómo manejarlo, puede lanzar una excepción para que una capa superior la intercepte.
✨ Creando Excepciones Personalizadas
A menudo, las clases de excepción estándar de Ruby no son lo suficientemente descriptivas para los errores específicos de tu aplicación. Crear tus propias clases de excepción te permite categorizar y manejar errores de una manera más granular y significativa.
Por qué usar Excepciones Personalizadas
- Claridad del Código: Los nombres de las excepciones pueden comunicar mejor la naturaleza del problema.
- Manejo Granular: Permite que diferentes bloques
rescuecapturen tipos específicos de errores de tu dominio. - Organización: Ayuda a estructurar la lógica de manejo de errores en aplicaciones grandes.
Pasos para Crear una Excepción Personalizada
- Heredar de
StandardError(o una de sus subclases): Es la mejor práctica para la mayoría de los casos. - Definir un constructor
initialize(opcional): Para agregar propiedades personalizadas al objeto de excepción.
# Definición de una excepción base para nuestra aplicación
class MiAplicacionError < StandardError
end
# Excepción específica para cuando un usuario no es encontrado
class UsuarioNoEncontradoError < MiAplicacionError
attr_reader :id_usuario
def initialize(message = "Usuario no encontrado", id_usuario = nil)
super(message)
@id_usuario = id_usuario
end
end
# Excepción específica para cuando una operación no está permitida
class OperacionNoPermitidaError < MiAplicacionError
attr_reader :operacion, :usuario
def initialize(message = "Operación no permitida", operacion = nil, usuario = nil)
super(message)
@operacion = operacion
@usuario = usuario
end
end
def buscar_usuario(id)
raise UsuarioNoEncontradoError.new("El usuario con ID #{id} no existe.", id) if id == 99
"Usuario con ID #{id} encontrado."
end
def realizar_accion(usuario, accion)
raise OperacionNoPermitidaError.new("El usuario '#{usuario}' no tiene permiso para '#{accion}'.", accion, usuario) if usuario == "invitado" && accion == "editar_perfil"
"#{usuario} realizó la acción '#{accion}'."
end
# Manejo de las excepciones personalizadas
begin
puts buscar_usuario(123)
puts buscar_usuario(99)
rescue UsuarioNoEncontradoError => e
puts "❌ Error de usuario: #{e.message} (ID Buscado: #{e.id_usuario})"
rescue MiAplicacionError => e # Captura cualquier otra excepción de nuestra aplicación
puts "❌ Error general de la aplicación: #{e.message}"
end
puts "---"
begin
puts realizar_accion("admin", "eliminar_datos")
puts realizar_accion("invitado", "editar_perfil")
rescue OperacionNoPermitidaError => e
puts "❌ Error de permiso: #{e.message} (Operación: #{e.operacion}, Usuario: #{e.usuario})"
end
📈 Buenas Prácticas y Estrategias Avanzadas
La gestión de errores no se trata solo de escribir begin/rescue, sino de hacerlo de manera efectiva y sostenible.
🎯 Principios Clave
- No suprimir errores en silencio: Evita bloques
rescuevacíos que ocultan problemas importantes. Si rescatas un error, haz algo con él (registra, notifica, recupera, etc.). - Rescata solo lo que puedes manejar: No uses
rescue StandardErrorindiscriminadamente si solo esperas unZeroDivisionError. Sé lo más específico posible. - Registra los errores: Utiliza un sistema de logging (
Loggerde Ruby,Rails.loggeren Rails) para registrar detalles de las excepciones, incluyendomessageybacktrace. - Diseña con la "Ley de Deméter" en mente: Un objeto debe hablar solo con sus amigos directos. Evita cadenas de llamadas que puedan fallar en múltiples puntos y sean difíciles de depurar (
a.b.c.d.e). Ruby tiene el operador safe navigation (&.) para esto.
# Sin safe navigation, podría lanzar NoMethodError si usuario es nil o no tiene perfil
# nombre_ciudad = usuario.perfil.direccion.ciudad
# Con safe navigation
nombre_ciudad = usuario&.perfil&.direccion&.ciudad
puts "Ciudad: #{nombre_ciudad || 'Desconocida'}"
Estrategias Comunes
- Centralización del Manejo de Errores: En aplicaciones grandes, puedes tener un módulo o clase dedicada a manejar errores, especialmente para casos comunes (e.g., errores de API).
- Patrón Null Object: En lugar de devolver
nilo lanzar una excepción cuando un objeto no se encuentra, devolver un "objeto nulo" que responde a los mismos métodos pero no hace nada o devuelve valores por defecto. Esto evitaNoMethodErroren cascada. - Circuit Breaker (Cortocircuito): Para servicios externos, un circuit breaker puede detener temporalmente las llamadas a un servicio que está fallando repetidamente, evitando sobrecargarlo aún más y dando tiempo para que se recupere.
- Idempotencia con
retry: Asegúrate de que las operaciones que reintentas conretrysean idempotentes (ejecutar la operación múltiples veces produce el mismo resultado que ejecutarla una vez) o que tu lógica de reintento maneje las ejecuciones duplicadas.
Ejemplo de uso de `Logger`
```ruby require 'logger'LOG = Logger.new(STDOUT) LOG.level = Logger::INFO
def procesar_datos_criticos(data) begin # Simular una operación que falla raise "Datos corruptos detectados" if data == :corruptos LOG.info "Datos procesados exitosamente." rescue => e LOG.error "Fallo al procesar datos: #{e.message}" LOG.error e.backtrace.join("\n") # Opcional: Notificar a un servicio de monitoreo # send_notification_to_bugsnag(e) end end
procesar_datos_criticos(:buenos) procesar_datos_criticos(:corruptos)
</details>
### Tabla Comparativa: `rescue` vs `if` para manejo de errores
| Característica | `rescue` (Excepciones) | `if` (Verificación de Valores de Retorno) |
| :------------------------- | :----------------------------------------------------- | :---------------------------------------------------- |
| --- | --- | --- |
| **Propósito Principal** | Manejar eventos *excepcionales* e inesperados. | Manejar condiciones *esperadas* y flujos alternativos. |
| **Flujo de Control** | Interrumpe el flujo normal; salta al bloque `rescue`. | Sigue un flujo condicional normal. |
| --- | --- | --- |
| **Legibilidad** | Más claro para errores, separa el "camino feliz" del error. | Puede llevar a `if anidados` o código disperso para errores. |
| **Rendimiento** | Tiene un *overhead* mayor, ya que involucra la pila de llamadas. | Bajo *overhead*, es solo una evaluación condicional. |
| --- | --- | --- |
| **Jerarquía de Errores** | Permite definir y atrapar tipos específicos de errores. | No aplica; se basa en valores de retorno. |
| **Recomendado para** | Condiciones de error irrecuperables, eventos inesperados. | Validaciones de entrada, valores de retorno esperados. |
<div class="callout warning">⚠️ <strong>Advertencia:</strong> No uses excepciones como un mecanismo de control de flujo estándar. Si puedes prever una condición y manejarla con una simple verificación `if`, es generalmente más eficiente y claro hacerlo así. Las excepciones están diseñadas para eventos *excepcionales*.</div>
---
## 📝 Ejemplos Prácticos Completos
Para consolidar lo aprendido, veamos un par de ejemplos más elaborados.
### <span class="badge green">Ejemplo 1: Procesamiento de un archivo CSV</span>
```ruby
require 'csv'
class ErrorProcesamientoCSV < StandardError
end
class ArchivoVacioError < ErrorProcesamientoCSV
def initialize(msg = "El archivo CSV está vacío.")
super
end
end
class FormatoInvalidoError < ErrorProcesamientoCSV
attr_reader :linea, :dato_problema
def initialize(msg = "Formato de línea CSV inválido", linea = nil, dato_problema = nil)
super(msg)
@linea = linea
@dato_problema = dato_problema
end
end
def procesar_csv(ruta_archivo)
datos_procesados = []
begin
raise Errno::ENOENT, "Archivo no encontrado: #{ruta_archivo}" unless File.exist?(ruta_archivo)
csv_data = CSV.read(ruta_archivo, headers: true)
raise ArchivoVacioError if csv_data.empty?
csv_data.each_with_index do |row, index|
begin
nombre = row['Nombre']
edad = Integer(row['Edad']) # Intentar convertir a entero, puede lanzar ArgumentError
ciudad = row['Ciudad']
raise FormatoInvalidoError.new("Edad no es un número", index + 2, row['Edad']) unless edad.is_a? Integer
datos_procesados << { nombre: nombre, edad: edad, ciudad: ciudad }
rescue ArgumentError => e # Captura error de Integer() conversion
raise FormatoInvalidoError.new("Error de conversión en línea #{index + 2}: #{e.message}", index + 2, row['Edad'])
rescue => e # Captura cualquier otro error en la fila
raise FormatoInvalidoError.new("Error inesperado en línea #{index + 2}: #{e.message}", index + 2, row.to_s)
end
end
puts "✅ CSV procesado exitosamente: #{datos_procesados.size} registros."
return datos_procesados
rescue Errno::ENOENT => e
puts "❌ Error: #{e.message}"
[]
rescue ArchivoVacioError => e
puts "❌ Advertencia: #{e.message}"
[]
rescue FormatoInvalidoError => e
puts "❌ Error de formato en CSV: #{e.message}"
puts " Línea: #{e.linea}, Dato problemático: '#{e.dato_problema}'"
[]
rescue ErrorProcesamientoCSV => e # Captura cualquier otra excepción personalizada
puts "❌ Error general de procesamiento CSV: #{e.message}"
[]
rescue StandardError => e # Captura cualquier otra excepción inesperada
puts "❌ Error inesperado al procesar CSV: #{e.class} - #{e.message}"
puts e.backtrace.join("\n")
[]
ensure
puts "Fin del intento de procesamiento de archivo."
end
end
# Simular archivos
File.write('datos_validos.csv', "Nombre,Edad,Ciudad\nAlice,30,NY\nBob,24,LA")
File.write('datos_invalidos.csv', "Nombre,Edad,Ciudad\nCharlie,veinte,Chicago\nDavid,40,Houston")
File.write('archivo_vacio.csv', "Nombre,Edad,Ciudad")
puts "\n--- Procesando datos_validos.csv ---"
procesar_csv('datos_validos.csv')
puts "\n--- Procesando datos_invalidos.csv ---"
procesar_csv('datos_invalidos.csv')
puts "\n--- Procesando archivo_vacio.csv ---"
procesar_csv('archivo_vacio.csv')
puts "\n--- Procesando archivo_no_existe.csv ---"
procesar_csv('archivo_no_existe.csv')
File.delete('datos_validos.csv') if File.exist?('datos_validos.csv')
File.delete('datos_invalidos.csv') if File.exist?('datos_invalidos.csv')
File.delete('archivo_vacio.csv') if File.exist?('archivo_vacio.csv')
Este ejemplo muestra cómo combinar múltiples bloques rescue, excepciones personalizadas y la verificación de condiciones para crear un robusto procesador de CSV.
Ejemplo 2: Un sistema de autenticación simplificado
class AuthError < StandardError
end
class CredencialesInvalidasError < AuthError
def initialize(msg = "Credenciales de usuario o contraseña incorrectas.")
super
end
end
class UsuarioBloqueadoError < AuthError
attr_reader :usuario_intento
def initialize(msg = "El usuario está bloqueado debido a múltiples intentos fallidos.", usuario = nil)
super(msg)
@usuario_intento = usuario
end
end
class SistemaAutenticacion
USUARIOS = {
"admin" => { password: "pass123", bloqueado: false },
"user" => { password: "secure", bloqueado: false },
"locked_user" => { password: "locked", bloqueado: true }
}
MAX_INTENTOS_FALLIDOS = 3
@@intentos_fallidos = Hash.new(0)
def self.autenticar(nombre_usuario, password)
begin
usuario_data = USUARIOS[nombre_usuario]
raise CredencialesInvalidasError unless usuario_data
raise UsuarioBloqueadoError.new("Usuario '#{nombre_usuario}' bloqueado.", nombre_usuario) if usuario_data[:bloqueado]
if usuario_data[:password] == password
@@intentos_fallidos[nombre_usuario] = 0 # Resetear intentos al éxito
puts "Bienvenido, #{nombre_usuario}!"
true
else
@@intentos_fallidos[nombre_usuario] += 1
if @@intentos_fallidos[nombre_usuario] >= MAX_INTENTOS_FALLIDOS
USUARIOS[nombre_usuario][:bloqueado] = true
raise UsuarioBloqueadoError.new("Usuario '#{nombre_usuario}' bloqueado tras #{MAX_INTENTOS_FALLIDOS} intentos fallidos.", nombre_usuario)
else
raise CredencialesInvalidasError
end
end
rescue CredencialesInvalidasError => e
puts "❌ Error de autenticación: #{e.message}"
false
rescue UsuarioBloqueadoError => e
puts "❌ Error de seguridad: #{e.message}"
false
rescue AuthError => e # Captura cualquier otra excepción personalizada de autenticación
puts "❌ Error inesperado de autenticación: #{e.message}"
false
rescue StandardError => e
puts "❌ Error crítico en el sistema de autenticación: #{e.class} - #{e.message}"
false
end
end
end
puts "\n--- Intentos de autenticación ---"
SistemaAutenticacion.autenticar("admin", "pass123") # Éxito
SistemaAutenticacion.autenticar("user", "wrong") # Falla 1
SistemaAutenticacion.autenticar("user", "wrong") # Falla 2
SistemaAutenticacion.autenticar("user", "wrong") # Falla 3, bloqueado
SistemaAutenticacion.autenticar("user", "secure") # Bloqueado
SistemaAutenticacion.autenticar("inexistente", "123") # Usuario no existe
SistemaAutenticacion.autenticar("locked_user", "locked") # Usuario pre-bloqueado
Este ejemplo demuestra cómo usar excepciones personalizadas para modelar diferentes estados y errores en un proceso de autenticación, mejorando la claridad y el control del flujo.
🔮 Conclusión
La gestión de errores es una parte integral del desarrollo de software de calidad. Al dominar el uso de begin/rescue/ensure/else/retry, raise, y la creación de excepciones personalizadas, puedes construir aplicaciones Ruby más robustas, confiables y fáciles de mantener. Recuerda siempre priorizar la claridad, la especificidad en el manejo de errores y la importancia de no suprimir problemas en silencio.
¡Felicidades, ahora estás equipado para escribir código Ruby que no solo funciona, sino que también maneja los imprevistos con gracia!
Tutoriales relacionados
- ¡Desatando el Potencial! Explorando los Decoradores de Métodos con `Module#prepend` en Rubyintermediate20 min
- Concurrencia en Ruby: Explorando Hilos, Ractor y Fibers para Aplicaciones Paralelasintermediate18 min
- ¡Maestría en Programación Funcional! Explorando Enumerable y sus Joyas en Rubyintermediate15 min
- Desbloqueando la Magia: Creando tus Propios DSLs en Ruby con Facilidadintermediate20 min
- Meta-programación en Ruby: Escribiendo Código que Escribe Códigoadvanced15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!