Refactoring, o refactorización, es modificar la estructura interna del código sin cambiar su comportamiento externo. El usuario del programa debería obtener los mismos resultados antes y después del cambio, pero el código debería quedar más fácil de entender, modificar y probar.
En este primer tema vamos a distinguir refactoring de corrección de errores y de agregado de funcionalidad. También haremos una práctica pequeña en Python para observar el ciclo básico: partir de código que funciona, cubrir su comportamiento con pruebas simples, aplicar cambios pequeños y verificar que todo sigue funcionando.
Un refactoring no busca que el programa haga algo nuevo. Busca que el código quede en mejores condiciones para el trabajo futuro. Por ejemplo, podemos renombrar variables, extraer funciones, simplificar condicionales, mover responsabilidades o separar entrada y salida de la lógica de negocio.
La idea central es conservar el comportamiento observable. Si una función recibía ciertos datos y devolvía cierto resultado, después del refactoring debe seguir devolviendo ese mismo resultado para esos mismos datos.
Conviene separar tres actividades que muchas veces se mezclan:
En un proyecto real se pueden hacer las tres cosas durante una misma jornada, pero no conviene mezclarlas en el mismo cambio. Si cambiamos la estructura y el comportamiento al mismo tiempo, cuando algo falla será más difícil encontrar la causa.
El refactoring tiene más valor cuando reduce el costo de un cambio próximo. No se trata de embellecer código por gusto, sino de preparar el código para trabajar con menos riesgo.
Algunas situaciones típicas son:
No todo código incómodo debe refactorizarse de inmediato. A veces es mejor esperar o hacer un cambio más pequeño.
En esos casos, el primer trabajo suele ser crear una red de seguridad: pruebas, ejemplos ejecutables o verificaciones manuales claras.
Vamos a trabajar con una función que calcula el total de una compra. El código funciona, pero mezcla varias decisiones en un solo bloque y usa nombres poco expresivos.
def calc(items, tipo):
total = 0
for i in items:
total += i["p"] * i["q"]
if tipo == "vip":
total = total - total * 0.15
elif tipo == "regular":
total = total - total * 0.05
total = total + total * 0.21
if total < 10000:
total = total + 1200
return total
Antes de modificarlo, necesitamos saber qué comportamiento debemos conservar.
Crea un archivo llamado test_refactoring_tema1.py. En este primer ejemplo usaremos pruebas simples con pytest para fijar el comportamiento actual.
from compra import calc
def test_calcula_total_para_cliente_vip_con_envio():
items = [
{"p": 3000, "q": 2},
{"p": 1500, "q": 1},
]
assert calc(items, "vip") == 8913.75
def test_calcula_total_para_cliente_sin_descuento_y_sin_envio():
items = [
{"p": 6000, "q": 2},
]
assert calc(items, "nuevo") == 14520
Estas pruebas no dicen que el diseño sea bueno. Solo documentan el resultado actual para que podamos cambiar la estructura con más seguridad.
Guarda la función original en un archivo llamado compra.py y ejecuta:
pytest test_refactoring_tema1.py
Si las pruebas pasan, tenemos una primera red de seguridad. Todavía es pequeña, pero alcanza para practicar el ciclo de refactoring.
El primer cambio será renombrar la función, los parámetros y las variables. No vamos a cambiar la fórmula ni los datos de entrada.
def calcular_total(items, tipo_cliente):
total = 0
for item in items:
total += item["p"] * item["q"]
if tipo_cliente == "vip":
total = total - total * 0.15
elif tipo_cliente == "regular":
total = total - total * 0.05
total = total + total * 0.21
if total < 10000:
total = total + 1200
return total
Después de este cambio, las pruebas deberían adaptarse para llamar a calcular_total. El comportamiento esperado sigue siendo el mismo.
Los números importantes del dominio no deberían quedar escondidos dentro de una función. Podemos darles nombre sin alterar el cálculo.
IVA = 0.21
DESCUENTO_VIP = 0.15
DESCUENTO_REGULAR = 0.05
LIMITE_ENVIO = 10000
COSTO_ENVIO = 1200
def calcular_total(items, tipo_cliente):
total = 0
for item in items:
total += item["p"] * item["q"]
if tipo_cliente == "vip":
total = total - total * DESCUENTO_VIP
elif tipo_cliente == "regular":
total = total - total * DESCUENTO_REGULAR
total = total + total * IVA
if total < LIMITE_ENVIO:
total = total + COSTO_ENVIO
return total
Ejecuta las pruebas después de este paso. En refactoring, cada mejora pequeña debería terminar con una verificación.
Ahora podemos separar partes del cálculo. El objetivo no es crear muchas funciones por costumbre, sino poner nombres a decisiones relevantes.
IVA = 0.21
DESCUENTO_VIP = 0.15
DESCUENTO_REGULAR = 0.05
LIMITE_ENVIO = 10000
COSTO_ENVIO = 1200
def calcular_subtotal(items):
total = 0
for item in items:
total += item["p"] * item["q"]
return total
def obtener_descuento(tipo_cliente):
if tipo_cliente == "vip":
return DESCUENTO_VIP
if tipo_cliente == "regular":
return DESCUENTO_REGULAR
return 0
def aplicar_envio(total):
if total < LIMITE_ENVIO:
return total + COSTO_ENVIO
return total
def calcular_total(items, tipo_cliente):
subtotal = calcular_subtotal(items)
descuento = obtener_descuento(tipo_cliente)
total_con_descuento = subtotal * (1 - descuento)
total_con_iva = total_con_descuento * (1 + IVA)
return aplicar_envio(total_con_iva)
La función principal ahora cuenta la historia del cálculo: subtotal, descuento, impuesto y envío. Las pruebas deben seguir pasando.
En el ejemplo anterior conservamos las claves "p" y "q" para no cambiar el formato de entrada. Cambiar esas claves por "precio" y "cantidad" podría ser una mejora, pero ya afecta el contrato con quien llama a la función.
Cuando un cambio modifica la forma de usar una función, conviene tratarlo con más cuidado. Puede seguir siendo parte de una mejora del diseño, pero ya no es un refactoring puramente interno si rompe llamadas existentes.
Un buen hábito de refactoring es avanzar en pasos pequeños:
Si algo falla, el último cambio fue pequeño y será más fácil entender qué ocurrió.
Analiza la siguiente función. Primero escribe qué resultado esperas para dos casos distintos. Luego refactoriza sin cambiar esos resultados.
def procesar(usuario):
r = 0
if usuario["activo"]:
r = r + 100
if usuario["compras"] > 10:
r = r + 50
if usuario["pais"] == "AR":
r = r + 20
return r
Una posible primera mejora consiste en dar nombres a la función, a la variable acumuladora y a las reglas de puntaje.
PUNTOS_USUARIO_ACTIVO = 100
PUNTOS_CLIENTE_FRECUENTE = 50
PUNTOS_PAIS_LOCAL = 20
MINIMO_COMPRAS_FRECUENTES = 10
PAIS_LOCAL = "AR"
def calcular_puntaje_usuario(usuario):
puntaje = 0
if usuario["activo"]:
puntaje += PUNTOS_USUARIO_ACTIVO
if usuario["compras"] > MINIMO_COMPRAS_FRECUENTES:
puntaje += PUNTOS_CLIENTE_FRECUENTE
if usuario["pais"] == PAIS_LOCAL:
puntaje += PUNTOS_PAIS_LOCAL
return puntaje
Crea un archivo llamado ejercicio_tema1.py con este código:
def f(pedido):
t = 0
for x in pedido["items"]:
t = t + x["precio"] * x["cantidad"]
if pedido["cliente"] == "premium":
t = t * 0.9
if pedido["urgente"]:
t = t + 1500
return t
Realiza estas tareas:
Una solución posible, manteniendo el mismo formato de datos, es la siguiente:
DESCUENTO_PREMIUM = 0.10
COSTO_PEDIDO_URGENTE = 1500
def calcular_subtotal(items):
subtotal = 0
for item in items:
subtotal += item["precio"] * item["cantidad"]
return subtotal
def aplicar_descuento_cliente(total, tipo_cliente):
if tipo_cliente == "premium":
return total * (1 - DESCUENTO_PREMIUM)
return total
def aplicar_costo_urgente(total, es_urgente):
if es_urgente:
return total + COSTO_PEDIDO_URGENTE
return total
def calcular_total_pedido(pedido):
subtotal = calcular_subtotal(pedido["items"])
total_con_descuento = aplicar_descuento_cliente(subtotal, pedido["cliente"])
return aplicar_costo_urgente(total_con_descuento, pedido["urgente"])
Esta versión no agrega nuevas reglas de negocio. Solo hace que las reglas actuales sean más visibles y más fáciles de probar por separado.
Antes de continuar con el próximo tema, verifica que puedes explicar estos puntos:
En este tema vimos que refactoring no significa cambiar lo que el programa hace, sino cómo está organizado internamente. También practicamos un ciclo básico en Python: escribir pruebas sobre el comportamiento actual, aplicar una mejora pequeña y verificar que el resultado se mantiene.
A partir del próximo tema prepararemos un proyecto de práctica para refactorizar de forma más ordenada, con estructura de archivos, pruebas automatizadas y herramientas que nos permitan avanzar con confianza.