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.
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.
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.
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.
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)
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.
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.
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
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},
]
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:
Si no estás seguro, puede ser razonable esperar hasta ver una tercera repetición o hasta conocer mejor el dominio.
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.
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)
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
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"
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()
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.
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.
En el proyecto ventas_demo, busca duplicación real o potencial y realiza estas tareas:
python -m ruff check src tests
python -m black src tests
python -m pytest
Antes de continuar, verifica que puedes hacer lo siguiente:
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.