Métodos en Ruby y bloques para reutilizar lógica

Capítulo 4

Tiempo estimado de lectura: 7 minutos

+ Ejercicio

Definir métodos: la unidad básica de reutilización

Un método encapsula una tarea con un nombre, recibe datos (parámetros) y devuelve un resultado. En Ruby, definir métodos es la forma más directa de evitar duplicación y hacer tu código más legible.

Sintaxis mínima

def saludar(nombre)
  "Hola, #{nombre}"
end

saludar("Ana") # => "Hola, Ana"

Observa que el método devuelve un valor aunque no uses return. Eso se llama retorno implícito.

Parámetros: posicionales, por defecto y keywords

1) Parámetros posicionales

Se asignan por orden. Úsalos cuando el significado sea obvio y el número de argumentos sea pequeño.

def area_rectangulo(ancho, alto)
  ancho * alto
end

area_rectangulo(5, 2) # => 10

2) Parámetros con valor por defecto

Permiten omitir argumentos comunes.

def formatear_precio(monto, moneda = "EUR")
  "#{monto} #{moneda}"
end

formatear_precio(10)         # => "10 EUR"
formatear_precio(10, "USD") # => "10 USD"

3) Parámetros keyword (con nombre)

Mejoran la claridad cuando hay varios parámetros opcionales o cuando el orden no debería importar.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

def crear_usuario(nombre:, email:, rol: "user")
  { nombre: nombre, email: email, rol: rol }
end

crear_usuario(nombre: "Ana", email: "ana@mail.com")
# => {:nombre=>"Ana", :email=>"ana@mail.com", :rol=>"user"}

Si olvidas un keyword requerido, Ruby lanza un error, lo cual ayuda a detectar fallos temprano.

Mezcla típica: posicional + keyword

def enviar_email(destinatario, asunto:, prioridad: "normal")
  "Enviando a #{destinatario} (#{prioridad}): #{asunto}"
end

enviar_email("ana@mail.com", asunto: "Bienvenida")

Retorno implícito y explícito

Retorno implícito (recomendado en la mayoría de casos)

Ruby devuelve la última expresión evaluada.

def descuento(monto)
  monto * 0.10
end

Retorno explícito con return

Útil para salir antes (guard clauses) o para dejar claro un “corte” en el flujo.

def precio_final(monto, cupon: nil)
  return monto if cupon.nil?

  if cupon == "PROMO10"
    monto * 0.90
  else
    monto
  end
end

Guía práctica: refactoriza un script en métodos reutilizables

Objetivo: tomar un código que calcula totales y aplicarle estructura. Imagina que tienes una lista de importes (por ejemplo, pedidos) y quieres: sumar, aplicar impuesto y formatear.

Paso 1: define métodos pequeños y con una sola responsabilidad

def subtotal(importes)
  importes.sum
end

def aplicar_impuesto(monto, tasa: 0.21)
  monto * (1 + tasa)
end

def formatear_moneda(monto, moneda: "EUR")
  "#{format('%.2f', monto)} #{moneda}"
end

Paso 2: compón los métodos para resolver el caso completo

def total_con_impuesto_formateado(importes, tasa: 0.21, moneda: "EUR")
  total = aplicar_impuesto(subtotal(importes), tasa: tasa)
  formatear_moneda(total, moneda: moneda)
end

importes = [10, 25.5, 4]

total_con_impuesto_formateado(importes)
# => "47.80 EUR"

Este estilo facilita pruebas, cambios y reutilización en otros contextos.

Bloques: personaliza comportamiento sin cambiar el método

Un bloque es un fragmento de código que puedes pasar a un método. Se escribe con { ... } o do ... end. Es ideal para “inyectar” lógica: transformaciones, filtros, callbacks, etc.

Bloques con iteradores: map, select, reduce

Estos métodos trabajan sobre colecciones y reciben un bloque.

  • map: transforma cada elemento
  • select: filtra elementos según una condición
  • reduce (o inject): acumula un resultado
numeros = [1, 2, 3, 4, 5]

cuadrados = numeros.map { |n| n * n }
# => [1, 4, 9, 16, 25]

pares = numeros.select { |n| n.even? }
# => [2, 4]

suma = numeros.reduce(0) { |acc, n| acc + n }
# => 15

yield: ejecutar el bloque dentro de tu método

Cuando defines un método, puedes “llamar” al bloque recibido usando yield. Esto te permite crear métodos reutilizables que delegan una parte variable al bloque.

Ejemplo: medir tiempo (callback simple)

def medir(etiqueta)
  inicio = Time.now
  resultado = yield
  fin = Time.now
  { etiqueta: etiqueta, segundos: (fin - inicio), resultado: resultado }
end

info = medir("cálculo") do
  (1..1_000_00).reduce(0) { |acc, n| acc + n }
end

info[:resultado]

Comprobar si hay bloque: block_given?

Si tu método puede funcionar con o sin bloque, valida antes de usar yield.

def con_log(mensaje)
  puts "[LOG] #{mensaje}"
  return yield if block_given?
  :sin_bloque
end

Actividad: método que recibe un bloque para personalizar un cálculo

Necesidad real: calcular un total, pero permitir que el usuario del método defina cómo se calcula cada línea (por ejemplo, aplicar descuento por producto, redondeos, etc.).

Paso a paso

  1. El método recibe una colección.
  2. Por defecto, usa el valor tal cual.
  3. Si hay bloque, el bloque decide el valor a sumar.
def total_personalizable(items)
  items.reduce(0) do |acc, item|
    valor = block_given? ? yield(item) : item
    acc + valor
  end
end

precios = [10, 20, 30]

# Sin bloque: suma normal
total_personalizable(precios) # => 60

# Con bloque: descuento del 10%
total_personalizable(precios) { |p| p * 0.9 } # => 54.0

Este patrón aparece en librerías y frameworks: el método ofrece una estructura y el bloque personaliza el detalle.

De bloque a objeto: Proc y lambda

Un bloque es “anónimo” y vive en la llamada. A veces necesitas guardar esa lógica en una variable, pasarla entre métodos o reutilizarla. Para eso existen Proc y lambda.

Cuándo usar cada uno (regla práctica)

HerramientaÚsala cuando...Nota clave
BloqueLa lógica se usa una sola vez en esa llamadaNo se guarda
ProcQuieres guardar un callback flexibleMás permisivo con argumentos
lambdaQuieres una “función” más estrictaValida aridad (número de argumentos)

Ejemplo mínimo: transformación configurable (reutilizable)

normalizar = ->(texto) { texto.strip.downcase }

nombres = ["  ANA ", "Bea", " carlos  "]

nombres.map(&normalizar)
# => ["ana", "bea", "carlos"]

El operador & convierte un Proc/lambda en bloque para métodos como map.

Ejemplo mínimo: callback (por ejemplo, “cuando termine, avisa”)

def procesar_pedido(id, on_success: nil)
  resultado = { id: id, estado: "ok" }
  on_success&.call(resultado)
  resultado
end

notificar = ->(res) { puts "Pedido #{res[:id]} procesado" }

procesar_pedido(123, on_success: notificar)

Este estilo es común cuando quieres hacer tu método extensible sin acoplarlo a una acción concreta (enviar email, loguear, actualizar UI, etc.).

Diferencia práctica entre Proc y lambda (aridad)

p1 = Proc.new { |a, b| [a, b] }
l1 = ->(a, b) { [a, b] }

p1.call(1)    # => [1, nil]
# l1.call(1)  # => ArgumentError (faltan argumentos)

Componer transformaciones sobre colecciones

Una forma moderna de escribir Ruby es encadenar transformaciones claras: primero filtras, luego transformas, luego agregas. Esto reduce estados intermedios y hace el flujo de datos evidente.

Ejemplo: pipeline con select + map + reduce

productos = [
  { nombre: "Teclado", precio: 30, stock: 3 },
  { nombre: "Mouse", precio: 15, stock: 0 },
  { nombre: "Monitor", precio: 120, stock: 2 }
]

total_inventario = productos
  .select { |p| p[:stock] > 0 }
  .map { |p| p[:precio] * p[:stock] }
  .reduce(0, :+)

total_inventario # => 30*3 + 120*2 = 330

Actividad: crea un “pipeline” configurable con lambdas

Necesidad real: aplicar una serie de transformaciones que pueden cambiar según el contexto (por ejemplo, normalización, validación, formateo).

def aplicar_pipeline(valor, pasos)
  pasos.reduce(valor) { |acc, paso| paso.call(acc) }
end

pasos = [
  ->(t) { t.strip },
  ->(t) { t.downcase },
  ->(t) { t.gsub(" ", "-") }
]

aplicar_pipeline("  Hola Mundo  ", pasos)
# => "hola-mundo"

Actividades propuestas (práctica guiada)

1) Refactorizar scripts previos en métodos reutilizables

  • Identifica 2–3 fragmentos repetidos (por ejemplo: calcular subtotal, formatear salida, validar datos).
  • Extrae cada fragmento a un método con nombre claro.
  • Reemplaza el código duplicado por llamadas a esos métodos.
  • Mejora la firma del método usando keywords para opciones (por ejemplo, moneda:, tasa:).

2) Crear un método que reciba un bloque para personalizar el cálculo

  • Implementa total_personalizable (o uno similar) para tu caso real: notas, precios, puntos, tiempos.
  • Haz que funcione sin bloque (comportamiento por defecto).
  • Agrega un ejemplo con bloque que cambie la regla (descuento, redondeo, penalización, etc.).

3) Componer transformaciones sobre colecciones

  • Elige una colección de hashes (por ejemplo, productos, usuarios, tareas).
  • Construye un pipeline: select para filtrar, map para transformar, reduce para agregar.
  • Extrae al menos una transformación a una lambda reutilizable y úsala con map(&mi_lambda) o dentro de un pipeline de pasos.

Ahora responde el ejercicio sobre el contenido:

En Ruby, ¿cuál es la forma correcta de permitir que un método personalice parte de su cálculo mediante un bloque, manteniendo un comportamiento por defecto cuando no se pasa ninguno?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Un patrón común es hacer que el método funcione sin bloque y, si se recibe uno, delegar la parte variable con yield. Para evitar errores cuando no hay bloque, se valida con block_given?.

Siguiente capítulo

Programación orientada a objetos en Ruby con clases y módulos

Arrow Right Icon
Portada de libro electrónico gratuitaRuby desde Cero para principiantes: Programación Moderna, Clara y Eficiente
40%

Ruby desde Cero para principiantes: Programación Moderna, Clara y Eficiente

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.