10. Duplicación de código y conocimiento repetido

10.1 Objetivo del tema

La duplicación es uno de los code smells más comunes. A veces se ve como líneas copiadas y pegadas; otras veces aparece como reglas de negocio repetidas con pequeñas variaciones. En ambos casos, el riesgo es el mismo: un cambio futuro puede aplicarse en un lugar y olvidarse en otro.

En este tema aprenderemos a detectar duplicación de código y duplicación de conocimiento. También veremos cuándo conviene eliminarla y cuándo es mejor esperar para no crear abstracciones prematuras.

Objetivo práctico: identificar duplicación en código Python y reemplazarla por constantes, funciones o estructuras de reglas sin cambiar el comportamiento.

10.2 Duplicación de código y duplicación de conocimiento

No toda duplicación es igual. La duplicación de código ocurre cuando dos fragmentos son muy parecidos. La duplicación de conocimiento ocurre cuando una misma regla está expresada en varios lugares, aunque el código no sea idéntico.

Ejemplo de duplicación de código:

if total < 10000:
    total += 1500

Si ese bloque aparece en varias funciones, hay una regla repetida: el costo de envío y el límite para envío gratis.

10.3 Por qué es peligrosa

La duplicación aumenta el costo de cambio. Si mañana el costo de envío pasa de 1500 a 1800, hay que encontrar todos los lugares donde aparece la regla.

El problema no es solo escribir más líneas. El verdadero problema es la inconsistencia: dos partes del sistema pueden empezar a comportarse de manera distinta sin que nadie lo haya decidido conscientemente.

La duplicación peligrosa no es repetir texto. Es repetir una decisión que debería existir en un único lugar confiable.

10.4 Ejemplo inicial

Observa estas dos funciones:

def calcular_total_minorista(productos):
    total = 0
    for producto in productos:
        total += producto["precio"] * producto["cantidad"]
    if total < 10000:
        total += 1500
    return total


def calcular_total_mayorista(productos):
    total = 0
    for producto in productos:
        total += producto["precio"] * producto["cantidad"]
    if total < 10000:
        total += 1500
    return total

Ambas funciones repiten el cálculo del subtotal y la regla de envío. Si la regla cambia, hay que modificar dos lugares.

10.5 Extraer una función compartida

Una primera mejora consiste en extraer el cálculo común:

LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500


def calcular_subtotal(productos):
    subtotal = 0
    for producto in productos:
        subtotal += producto["precio"] * producto["cantidad"]
    return subtotal


def aplicar_envio(total):
    if total < LIMITE_ENVIO_GRATIS:
        return total + COSTO_ENVIO
    return total

Ahora las funciones principales pueden reutilizar reglas con nombre:

def calcular_total_minorista(productos):
    subtotal = calcular_subtotal(productos)
    return aplicar_envio(subtotal)


def calcular_total_mayorista(productos):
    subtotal = calcular_subtotal(productos)
    return aplicar_envio(subtotal)

10.6 Duplicación con pequeñas diferencias

La duplicación más peligrosa suele tener pequeñas diferencias. Eso dificulta decidir si las reglas son realmente iguales o si representan casos distintos.

def calcular_envio_argentina(total):
    if total < 10000:
        return 1500
    return 0


def calcular_envio_uruguay(total):
    if total < 10000:
        return 1800
    return 0

Aquí la regla se parece, pero el costo cambia. Antes de unificar, hay que entender si esa diferencia es intencional.

10.7 Usar estructuras de reglas

Si la diferencia es parte del negocio, podemos representarla explícitamente:

LIMITE_ENVIO_GRATIS = 10000
COSTOS_ENVIO = {
    "AR": 1500,
    "UY": 1800,
}
COSTO_ENVIO_PREDETERMINADO = 2000


def calcular_envio(total, pais):
    if total >= LIMITE_ENVIO_GRATIS:
        return 0
    return COSTOS_ENVIO.get(pais, COSTO_ENVIO_PREDETERMINADO)

La regla queda en un solo lugar y las diferencias entre países quedan visibles.

10.8 Duplicación de validaciones

Las validaciones repetidas también son duplicación de conocimiento.

def registrar_usuario(usuario):
    if usuario["email"] == "":
        return "email obligatorio"
    if "@" not in usuario["email"]:
        return "email inválido"
    return "ok"


def actualizar_usuario(usuario):
    if usuario["email"] == "":
        return "email obligatorio"
    if "@" not in usuario["email"]:
        return "email inválido"
    return "ok"

Podemos extraer la validación común:

def validar_email(email):
    if email == "":
        return "email obligatorio"
    if "@" not in email:
        return "email inválido"
    return None

10.9 Duplicación en pruebas

Las pruebas también pueden tener duplicación. Una parte es aceptable si mejora claridad, pero demasiada repetición hace que las pruebas sean difíciles de mantener.

def test_cliente_vip_argentina():
    productos = [
        {"precio": 3000, "cantidad": 2},
        {"precio": 1500, "cantidad": 1},
    ]
    assert calcular_total_venta(productos, "vip", "AR") == 9213.75


def test_cliente_regular_argentina():
    productos = [
        {"precio": 3000, "cantidad": 2},
        {"precio": 1500, "cantidad": 1},
    ]
    assert calcular_total_venta(productos, "regular", "AR") == 10118.75

Podemos extraer un helper si los datos se repiten mucho:

def productos_de_ejemplo():
    return [
        {"precio": 3000, "cantidad": 2},
        {"precio": 1500, "cantidad": 1},
    ]

10.10 Cuidado con abstracciones prematuras

No toda repetición debe eliminarse inmediatamente. Si dos fragmentos se parecen pero podrían evolucionar en direcciones distintas, unificarlos demasiado pronto puede crear una abstracción confusa.

Repite esta pregunta antes de extraer:

¿Estos dos fragmentos repiten la misma decisión de negocio o solo se parecen por casualidad?

Si no estás seguro, puede ser razonable esperar hasta ver una tercera repetición o hasta conocer mejor el dominio.

10.11 Regla práctica: duplicación accidental y duplicación intencional

La duplicación accidental ocurre cuando copiamos lógica por comodidad. La duplicación intencional puede aparecer cuando dos casos se parecen hoy, pero pertenecen a reglas distintas.

Ejemplo: un descuento de 0.10 para estudiantes y otro de 0.10 para jubilados pueden ser iguales hoy, pero cambiar por motivos distintos. Unificarlos bajo una sola constante llamada DESCUENTO_GENERAL podría ocultar dos reglas diferentes.

10.12 Aplicación sobre ventas_demo

En ventas_demo, revisa si las reglas de descuento, impuesto y envío aparecen repetidas. Una posible organización es:

DESCUENTOS = {
    "vip": 0.15,
    "regular": 0.05,
}

IMPUESTOS = {
    "AR": 0.21,
    "UY": 0.22,
}

LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500

Luego usa funciones pequeñas para acceder a esas reglas:

def obtener_descuento(cliente):
    return DESCUENTOS.get(cliente, 0)


def obtener_impuesto(pais):
    return IMPUESTOS.get(pais, 0.19)

10.13 Proteger la eliminación de duplicación

Eliminar duplicación puede cambiar comportamiento si entendimos mal las diferencias. Antes de unificar reglas, conviene tener pruebas de los casos principales.

def test_obtener_descuento_para_cliente_vip():
    assert obtener_descuento("vip") == 0.15


def test_obtener_descuento_para_cliente_desconocido():
    assert obtener_descuento("nuevo") == 0

Después de modificar, ejecuta:

python -m pytest

10.14 Duplicación en mensajes

Los mensajes repetidos también pueden generar inconsistencias.

def validar_nombre(nombre):
    if nombre == "":
        return "El campo es obligatorio"
    return None


def validar_email(email):
    if email == "":
        return "El campo es obligatorio"
    return None

Podemos extraer una constante si el mensaje representa una regla común:

MENSAJE_CAMPO_OBLIGATORIO = "El campo es obligatorio"

10.15 Duplicación en transformaciones

Otra forma común de duplicación aparece al normalizar datos:

email = usuario["email"].strip().lower()
email_contacto = contacto["email"].strip().lower()

Si esa transformación es una regla del sistema, puede tener nombre propio:

def normalizar_email(email):
    return email.strip().lower()

10.16 Ejercicio guiado

Analiza este código y elimina duplicación con cuidado:

def calcular_total_online(productos):
    total = 0
    for producto in productos:
        total += producto["precio"] * producto["cantidad"]
    if total < 10000:
        total += 1500
    return total


def calcular_total_sucursal(productos):
    total = 0
    for producto in productos:
        total += producto["precio"] * producto["cantidad"]
    if total < 10000:
        total += 800
    return total

La suma de productos está duplicada. La regla de envío se parece, pero tiene costos distintos. Una mejora posible es extraer subtotal y parametrizar el costo de envío.

10.17 Una posible solución

LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO_ONLINE = 1500
COSTO_ENVIO_SUCURSAL = 800


def calcular_subtotal(productos):
    subtotal = 0
    for producto in productos:
        subtotal += producto["precio"] * producto["cantidad"]
    return subtotal


def aplicar_envio(total, costo_envio):
    if total < LIMITE_ENVIO_GRATIS:
        return total + costo_envio
    return total


def calcular_total_online(productos):
    subtotal = calcular_subtotal(productos)
    return aplicar_envio(subtotal, COSTO_ENVIO_ONLINE)


def calcular_total_sucursal(productos):
    subtotal = calcular_subtotal(productos)
    return aplicar_envio(subtotal, COSTO_ENVIO_SUCURSAL)

La solución conserva la diferencia entre online y sucursal, pero evita repetir el cálculo del subtotal y el límite de envío gratis.

10.18 Ejercicio propuesto

En el proyecto ventas_demo, busca duplicación real o potencial y realiza estas tareas:

  • Identifica qué regla o decisión se repite.
  • Decide si la duplicación es accidental o intencional.
  • Extrae una constante, función o estructura de datos si corresponde.
  • Agrega una prueba que proteja la regla extraída.
  • Ejecuta herramientas y pruebas.
python -m ruff check src tests
python -m black src tests
python -m pytest

10.19 Lista de verificación

Antes de continuar, verifica que puedes hacer lo siguiente:

  • Diferenciar duplicación de código y duplicación de conocimiento.
  • Detectar reglas de negocio repetidas.
  • Extraer constantes para valores repetidos con significado.
  • Extraer funciones para lógica compartida.
  • Usar diccionarios para representar tablas de reglas simples.
  • Evitar abstracciones prematuras.
  • Proteger cambios con pruebas antes de eliminar duplicación.

10.20 Conclusión

En este tema vimos que la duplicación no es solo repetir líneas, sino repetir conocimiento que debería estar en un único lugar. Aprendimos a extraer constantes, funciones y estructuras de reglas, cuidando no ocultar diferencias reales del dominio.

En el próximo tema trabajaremos con condicionales complejos, anidamiento profundo y expresiones difíciles de leer.