7. Code smells: concepto, señales y forma práctica de diagnosticarlos

7.1 Objetivo del tema

Un code smell, u olor de código, es una señal de que el código puede tener un problema de diseño, legibilidad o mantenibilidad. No siempre es un error directo, pero indica que conviene revisar con atención.

En este tema aprenderemos a diagnosticar code smells de forma práctica: observar síntomas, formular hipótesis, evaluar riesgo y decidir si conviene intervenir ahora o dejar una nota para más adelante.

Objetivo práctico: identificar code smells en fragmentos Python, describir el riesgo que generan y proponer una mejora razonable sin cambiar el comportamiento esperado.

7.2 Code smell no significa código incorrecto

Un código puede funcionar y aun así oler mal. La diferencia es importante: un bug demuestra que el comportamiento es incorrecto; un code smell sugiere que el código puede ser difícil de entender, probar o modificar.

Observa este ejemplo:

def calcular(a, b, c, d):
    if d:
        return a * b - c
    return a * b

La función puede devolver resultados correctos, pero los nombres no explican la intención y el parámetro d actúa como una bandera poco clara. Eso no prueba un bug, pero sí señala riesgo de mantenimiento.

7.3 Señales frecuentes

Al revisar código Python, algunas señales aparecen con frecuencia:

  • Funciones largas que mezclan varios pasos.
  • Nombres abreviados o demasiado genéricos.
  • Valores mágicos sin nombre propio.
  • Condicionales profundos o difíciles de seguir.
  • Código duplicado en varias funciones.
  • Clases con demasiadas responsabilidades.
  • Dependencias externas mezcladas con reglas de negocio.
  • Excepciones capturadas y silenciadas.

7.4 Diagnosticar no es adivinar

Un diagnóstico útil debe ser concreto. No alcanza con decir “este código está feo”. Conviene describir qué se observa, qué riesgo produce y qué mejora podría reducir ese riesgo.

Ejemplo de diagnóstico pobre:

Esta función está mal.

Ejemplo de diagnóstico útil:

La función calcula subtotal, descuento, impuestos y envío. Mezcla cuatro responsabilidades y por eso será difícil modificar una regla sin afectar las demás.

7.5 Una plantilla simple de diagnóstico

Podemos usar una estructura breve para registrar smells:

  • Señal: qué vemos en el código.
  • Riesgo: qué problema puede generar.
  • Evidencia: dónde aparece.
  • Acción posible: qué cambio reduciría el riesgo.

Aplicado al proyecto ventas_demo:

Señal: calcular_total_venta realiza varios cálculos en una sola función. Riesgo: cambiar impuestos o descuentos puede romper otras reglas. Evidencia: la función calcula subtotal, descuento, impuesto, envío y redondeo. Acción posible: separar funciones pequeñas para cada regla.

7.6 Smell de nombres poco claros

Los nombres pobres obligan a interpretar el código línea por línea. Este smell aparece cuando una variable o función no comunica su intención.

def p(x):
    r = []
    for i in x:
        if i["a"]:
            r.append(i["e"])
    return r

Una versión más clara puede ser:

def obtener_emails_activos(usuarios):
    emails = []
    for usuario in usuarios:
        if usuario["activo"]:
            emails.append(usuario["email"])
    return emails

El comportamiento puede ser el mismo, pero la segunda versión reduce el esfuerzo de lectura.

7.7 Smell de función larga

Una función larga no es mala solo por tener muchas líneas. El problema aparece cuando contiene varias responsabilidades o varios niveles de decisión.

def procesar_pedido(pedido):
    total = 0
    for producto in pedido["productos"]:
        total += producto["precio"] * producto["cantidad"]

    if pedido["cliente"] == "vip":
        total -= total * 0.15

    if pedido["pais"] == "AR":
        total += total * 0.21

    if total < 10000:
        total += 1500

    print(f"Total: {total}")
    return round(total, 2)

Esta función calcula, aplica reglas, imprime y redondea. El smell está en la mezcla de responsabilidades.

7.8 Smell de valores mágicos

Un valor mágico es un número o texto importante que aparece sin explicar su significado.

if total < 10000:
    total += 1500

Una versión más clara usa constantes:

LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500


if total < LIMITE_ENVIO_GRATIS:
    total += COSTO_ENVIO

Además de mejorar lectura, las constantes reducen la duplicación cuando la misma regla aparece en varios lugares.

7.9 Smell de condicional complejo

Los condicionales complejos son difíciles de revisar y fáciles de romper al modificarlos.

if usuario["activo"] and usuario["edad"] >= 18 and usuario["email"] != "" and not usuario["bloqueado"]:
    enviar_promocion(usuario)

Podemos nombrar la condición:

es_usuario_habilitado = (
    usuario["activo"]
    and usuario["edad"] >= 18
    and usuario["email"] != ""
    and not usuario["bloqueado"]
)

if es_usuario_habilitado:
    enviar_promocion(usuario)

El próximo paso podría ser extraer una función con un nombre de dominio, pero en este tema nos concentramos en reconocer la señal.

7.10 Smell de duplicación

La duplicación es peligrosa porque una regla puede cambiarse en un lugar y olvidarse en otro.

def calcular_total_minorista(total):
    if total < 10000:
        total += 1500
    return total


def calcular_total_mayorista(total):
    if total < 10000:
        total += 1500
    return total

Si cambia el costo de envío, hay que recordar modificar ambas funciones. Esa repetición es una señal clara de riesgo.

7.11 Smell de dependencia mezclada con lógica

Cuando una función mezcla reglas de negocio con lectura de archivos, red, fecha actual o consola, se vuelve más difícil de probar y modificar.

from datetime import date


def generar_factura(productos):
    total = sum(producto["precio"] for producto in productos)
    nombre_archivo = f"factura-{date.today().isoformat()}.txt"
    with open(nombre_archivo, "w", encoding="utf-8") as archivo:
        archivo.write(str(total))
    return total

El cálculo del total está mezclado con fecha actual y escritura en disco. Si solo queremos probar el cálculo, la función nos obliga a manejar archivos.

7.12 Smell de captura silenciosa de errores

Capturar una excepción y no hacer nada puede ocultar fallas reales.

def cargar_precio(datos):
    try:
        return float(datos["precio"])
    except Exception:
        return 0

El problema no es usar try. El problema es capturar cualquier error sin distinguir si faltó la clave, si el valor tenía formato inválido o si ocurrió otro problema inesperado.

7.13 Diagnóstico sobre ventas_demo

Revisa la función calcular_total_venta del proyecto ventas_demo. Aunque ya mejoramos nombres y estilo, todavía puede contener smells de diseño.

Preguntas útiles:

  • ¿La función tiene más de una responsabilidad?
  • ¿Hay reglas de negocio mezcladas en un solo bloque?
  • ¿Los impuestos deberían estar en una estructura de datos?
  • ¿Los descuentos deberían calcularse en una función separada?
  • ¿El redondeo pertenece a la misma función que calcula reglas?

No corrijas todo todavía. El objetivo es diagnosticar con precisión.

7.14 Priorizar smells

No todos los smells tienen la misma urgencia. En un proyecto real, conviene priorizar según riesgo, frecuencia de cambio y cercanía con una tarea actual.

  • Alto impacto: una función crítica cambia seguido y tiene muchas responsabilidades.
  • Impacto medio: una regla está duplicada en dos lugares, pero cambia pocas veces.
  • Bajo impacto: un nombre podría ser mejor, pero el código es estable y poco usado.
No todo smell exige acción inmediata. Un buen diagnóstico ayuda a decidir cuándo intervenir.

7.15 Herramientas y criterio humano

Ruff, Black e isort ayudan a detectar o corregir problemas concretos, pero muchos code smells requieren criterio humano. Una herramienta puede señalar una variable no usada, pero no siempre puede decidir si una función tiene demasiadas responsabilidades de negocio.

Usa herramientas para reducir ruido y usa revisión humana para entender intención, dominio y riesgos de cambio.

7.16 Ejercicio guiado

Analiza el siguiente código y registra al menos cuatro smells usando la plantilla señal, riesgo, evidencia y acción posible.

def hacer(datos):
    res = []
    for d in datos:
        try:
            if d["a"] == 1 and d["m"] != "":
                valor = d["p"] * d["c"]
                if valor > 5000:
                    valor = valor - 300
                res.append({"m": d["m"], "v": valor})
        except Exception:
            pass
    return res

Algunas señales posibles son nombres poco claros, valores mágicos, condicional complejo, captura silenciosa de errores y mezcla de validación con cálculo.

7.17 Una posible mejora

Una versión más clara podría ser:

ESTADO_ACTIVO = 1
LIMITE_DESCUENTO = 5000
DESCUENTO = 300


def calcular_importe(precio, cantidad):
    importe = precio * cantidad
    if importe > LIMITE_DESCUENTO:
        return importe - DESCUENTO
    return importe


def obtener_resultados_validos(datos):
    resultados = []
    for item in datos:
        if item["activo"] != ESTADO_ACTIVO:
            continue
        if item["email"] == "":
            continue

        importe = calcular_importe(item["precio"], item["cantidad"])
        resultados.append({"email": item["email"], "importe": importe})

    return resultados

Esta mejora no pretende ser definitiva. Su objetivo es mostrar cómo un diagnóstico concreto orienta cambios concretos.

7.18 Ejercicio propuesto

Elige una función de ventas_demo y realiza estas tareas:

  • Identifica al menos tres smells.
  • Describe la señal y el riesgo de cada uno.
  • Marca cuál corregirías primero y por qué.
  • Propón un cambio pequeño que reduzca el riesgo.
  • Ejecuta las pruebas antes y después del cambio.

Usa este comando para verificar comportamiento:

python -m pytest

7.19 Lista de verificación

Antes de continuar, verifica que puedes hacer lo siguiente:

  • Explicar qué es un code smell.
  • Diferenciar un smell de un bug.
  • Detectar nombres poco claros, valores mágicos y duplicación.
  • Reconocer funciones con responsabilidades mezcladas.
  • Describir un smell con señal, riesgo, evidencia y acción posible.
  • Priorizar smells según impacto y frecuencia de cambio.
  • Usar pruebas para proteger el comportamiento al mejorar código.

7.20 Conclusión

En este tema estudiamos los code smells como señales de riesgo. Vimos que no siempre indican errores inmediatos, pero ayudan a anticipar problemas de mantenimiento, legibilidad y diseño.

En el próximo tema profundizaremos en uno de los smells más comunes: funciones largas y funciones que hacen demasiadas cosas.