Código limpio en Ruby: estilo, legibilidad y refactorización

Capítulo 8

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Criterios de claridad en Ruby

El código limpio no es “código corto”: es código fácil de leer, cambiar y probar. En Ruby, la legibilidad suele mejorar cuando el código se acerca a un estilo declarativo (decir qué queremos) y reduce el ruido (decir cómo en exceso). A continuación tienes criterios prácticos para evaluar y mejorar claridad.

Nombres expresivos (intención primero)

Un buen nombre reduce la necesidad de comentarios y evita malentendidos. En Ruby, aprovecha nombres de métodos que suenen a pregunta para booleanos y nombres de variables que describan el rol, no el tipo.

  • Booleanos: active?, expired?, valid?
  • Métodos con efecto: verbos claros: calculate_total, apply_discount, normalize_email
  • Evita abreviaturas ambiguas: cfg, tmp, arr (salvo convenciones muy conocidas)
# Menos claro
u = users.select { |x| x[:a] }

# Más claro
active_users = users.select { |user| user[:active] }

Funciones pequeñas y con una sola responsabilidad

Un método pequeño facilita pruebas, reutilización y cambios. Señales de alerta: demasiados niveles de indentación, mezcla de validación + cálculo + formateo, o variables temporales que “transportan” estado entre bloques.

  • Un método debería poder describirse en una frase.
  • Si necesitas comentarios tipo “Paso 1/2/3”, probablemente son métodos por extraer.
  • Prefiere devolver valores en lugar de mutar estructuras externas.

Evitar duplicación (DRY con criterio)

Duplicación no es solo copiar/pegar: también es repetir la misma idea con pequeñas variaciones. En Ruby, suele resolverse extrayendo métodos, usando enumerables o encapsulando reglas en objetos.

# Duplicación de regla de negocio
if user[:country] == "ES"
  tax = subtotal * 0.21
else
  tax = subtotal * 0.10
end

# Extraer regla
rate = tax_rate_for(user)
tax  = subtotal * rate

Coherencia en estructuras de control

La coherencia reduce la carga cognitiva. Decide un estilo y aplícalo de forma consistente:

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

  • Preferir guard clauses (retornos tempranos) para evitar anidación profunda.
  • Evitar else cuando un retorno temprano lo hace innecesario.
  • Usar case cuando hay múltiples ramas por el mismo criterio.
# Anidación innecesaria
if user
  if user[:active]
    send_email(user)
  end
end

# Guard clauses
return unless user
return unless user[:active]
send_email(user)

Uso idiomático de enumerables

Ruby brilla cuando transformas colecciones con map, filtras con select/reject, agregas con reduce y consultas con any?/all?/none?. Esto suele reemplazar bucles manuales, variables acumuladoras y condicionales repetidos.

ObjetivoPreferirEvitar
TransformarmapConstruir arrays con << en bucles
Filtrarselect, rejectif dentro de bucles para decidir si agregar
Sumar/combinarreduce, sumAcumuladores manuales
BuscarfindRecorrer todo y guardar “el último encontrado”
# Imperativo
sum = 0
items.each do |item|
  if item[:active]
    sum += item[:price]
  end
end

# Declarativo
sum = items.select { |i| i[:active] }.sum { |i| i[:price] }

Refactorizaciones típicas (con ejemplos)

1) Reemplazar condicionales anidados por guard clauses

Cuando hay validaciones previas, los retornos tempranos hacen el “camino feliz” más visible.

# Antes
def process(order)
  if order
    if order[:items] && !order[:items].empty?
      if order[:paid]
        "OK"
      else
        "UNPAID"
      end
    else
      "EMPTY"
    end
  else
    "NO_ORDER"
  end
end

# Después
def process(order)
  return "NO_ORDER" unless order
  return "EMPTY" if order[:items].nil? || order[:items].empty?
  return "UNPAID" unless order[:paid]
  "OK"
end

2) Extraer métodos para nombrar decisiones

Extraer métodos no solo reduce tamaño: convierte lógica en vocabulario del dominio.

# Antes
if user[:active] && !user[:banned] && user[:email].to_s.include?("@")
  notify(user)
end

# Después
def notifiable?(user)
  user[:active] && !user[:banned] && user[:email].to_s.include?("@")
end

notify(user) if notifiable?(user)

3) Reducir variables temporales

Las variables temporales encadenadas suelen indicar que puedes componer expresiones o extraer un método. Mantén variables solo si aportan significado (nombre) o evitan repetir un cálculo costoso.

# Antes
subtotal = items.sum { |i| i[:price] * i[:qty] }
discount = subtotal * discount_rate
final = subtotal - discount

# Después (si el significado es obvio)
final = items.sum { |i| i[:price] * i[:qty] } * (1 - discount_rate)

Si el cálculo necesita nombre, extrae:

def subtotal_for(items)
  items.sum { |i| i[:price] * i[:qty] }
end

final = subtotal_for(items) * (1 - discount_rate)

4) Simplificar colecciones con map/select/reduce

Busca patrones: “recorrer y construir”, “recorrer y filtrar”, “recorrer y acumular”. Cambiarlos a enumerables reduce ruido y errores.

# Antes
emails = []
users.each do |u|
  if u[:active]
    emails << u[:email].to_s.strip.downcase
  end
end

# Después
emails = users.select { |u| u[:active] }
              .map { |u| u[:email].to_s.strip.downcase }

Sesión guiada de refactorización: de script “desordenado” a código claro

En esta práctica vas a refactorizar un script que calcula un reporte de pedidos. El objetivo no es cambiar el comportamiento, sino mejorar legibilidad, reducir duplicación y usar enumerables de forma idiomática.

Script provisto (antes)

# Script desordenado: reporte de pedidos
# Entrada: orders (Array de Hashes)
# Cada order: { id:, customer:, status:, items: [ { name:, price:, qty: } ] }

def report(orders)
  out = ""
  total = 0
  i = 0
  while i < orders.length
    o = orders[i]
    if o != nil
      if o[:status] == "paid"
        s = 0
        j = 0
        while j < o[:items].length
          it = o[:items][j]
          if it != nil
            s = s + (it[:price] * it[:qty])
          end
          j = j + 1
        end
        total = total + s
        out = out + "Order #" + o[:id].to_s + " - " + o[:customer].to_s + " - " + s.to_s + "\n"
      else
        out = out + "Order #" + o[:id].to_s + " skipped\n"
      end
    end
    i = i + 1
  end
  out = out + "TOTAL=" + total.to_s
  out
end

Checklist de mejoras (úsala como guía)

  • Nombres: ¿o, s, it expresan intención? Renombrar a order, subtotal, item.
  • Anidación: ¿hay if dentro de if dentro de bucles? Aplicar guard clauses o separar caminos.
  • Duplicación: ¿se repite lógica de formateo o cálculo? Extraer métodos.
  • Enumerables: reemplazar while + índices por each, map, sum, select.
  • Nil-checks: si el contrato de datos garantiza arrays/hashes válidos, elimina checks redundantes; si no, encapsula la tolerancia a nil en un lugar.
  • Construcción de strings: evitar concatenación repetida; preferir lines = [] y luego lines.join o join("\n").
  • Separación de responsabilidades: cálculo de subtotal, selección de pedidos y formateo en métodos distintos.

Paso a paso de refactorización

Paso 1: Cambiar índices por enumerables y mejorar nombres

def report(orders)
  out = ""
  total = 0

  orders.each do |order|
    next if order.nil?

    if order[:status] == "paid"
      subtotal = 0
      order[:items].each do |item|
        next if item.nil?
        subtotal += item[:price] * item[:qty]
      end
      total += subtotal
      out << "Order ##{order[:id]} - #{order[:customer]} - #{subtotal}\n"
    else
      out << "Order ##{order[:id]} skipped\n"
    end
  end

  out << "TOTAL=#{total}"
end

Paso 2: Extraer el cálculo de subtotal

Nombrar el cálculo reduce ruido en el reporte.

def order_subtotal(order)
  items = order[:items] || []
  items.compact.sum { |item| item[:price] * item[:qty] }
end

Ahora el reporte se enfoca en el flujo principal:

def report(orders)
  out = ""
  total = 0

  orders.compact.each do |order|
    if order[:status] == "paid"
      subtotal = order_subtotal(order)
      total += subtotal
      out << "Order ##{order[:id]} - #{order[:customer]} - #{subtotal}\n"
    else
      out << "Order ##{order[:id]} skipped\n"
    end
  end

  out << "TOTAL=#{total}"
end

Paso 3: Reemplazar condicionales por guard clauses y separar “líneas”

Construir un array de líneas suele ser más claro que concatenar en cada iteración.

def paid?(order)
  order[:status] == "paid"
end

def order_line(order, subtotal)
  "Order ##{order[:id]} - #{order[:customer]} - #{subtotal}"
end

def skipped_line(order)
  "Order ##{order[:id]} skipped"
end

def report(orders)
  lines = []
  total = 0

  orders.compact.each do |order|
    unless paid?(order)
      lines << skipped_line(order)
      next
    end

    subtotal = order_subtotal(order)
    total += subtotal
    lines << order_line(order, subtotal)
  end

  lines << "TOTAL=#{total}"
  lines.join("\n")
end

Paso 4: Simplificar aún más con map/select/sum (opcional)

Si quieres un estilo más declarativo, puedes construir líneas y total con enumerables. Úsalo si tu equipo lo encuentra más legible.

def report(orders)
  safe_orders = orders.compact

  lines = safe_orders.map do |order|
    if paid?(order)
      subtotal = order_subtotal(order)
      order_line(order, subtotal)
    else
      skipped_line(order)
    end
  end

  total = safe_orders.select { |o| paid?(o) }
                     .sum { |o| order_subtotal(o) }

  (lines + ["TOTAL=#{total}"]).join("\n")
end

Comparación antes/después (qué mejoró y por qué)

AspectoAntesDespués
LegibilidadVariables crípticas (o, s, it) y bucles con índicesNombres con intención y enumerables
AnidaciónMúltiples if anidados dentro de whileGuard clauses y rutas claras
ResponsabilidadesCálculo + formateo + acumulación mezcladosMétodos extraídos: order_subtotal, order_line, paid?
Duplicación/ruidoConcatenación repetida y checks dispersosConstrucción de líneas y compactación centralizada
Estilo RubyImperativo (índices, acumuladores manuales)Idiomas Ruby: each, sum, map, select, compact

Ahora responde el ejercicio sobre el contenido:

Al refactorizar un script Ruby que genera un reporte de pedidos, ¿qué cambio mejora más la claridad sin alterar el comportamiento, separando responsabilidades y reduciendo ruido?

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

¡Tú error! Inténtalo de nuevo.

Separar cálculo, decisiones y formateo en métodos reduce mezcla de responsabilidades. Las guard clauses hacen más visible el “camino feliz” y evitan anidación, y construir lines + join elimina concatenación repetida y ruido.

Siguiente capítulo

Construcción de aplicaciones de consola en Ruby con diseño modular

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

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.