Una clase grande suele empezar como una solución cómoda: todo está en un solo lugar. Con el tiempo acumula cálculo, validación, formato, persistencia, configuración y coordinación. El resultado es una clase difícil de probar y de modificar.
En este tema aprenderemos a dividir una clase grande en responsabilidades pequeñas. Usaremos composición y una fachada para mantener una interfaz estable mientras movemos comportamiento a clases más específicas.
Una clase puede estar acumulando demasiadas responsabilidades cuando:
Crea el archivo src/servicio_pedidos.py:
class ServicioPedidos:
def __init__(self):
self.historial = []
def calcular_total(self, pedido):
total = 0
for item in pedido["items"]:
total = total + item["precio"] * item["cantidad"]
if pedido["cliente"]["tipo"] == "vip":
total = total * 0.9
if pedido["canal"] == "online":
total = total + 500
return round(total, 2)
def crear_resumen(self, pedido):
total = self.calcular_total(pedido)
return {
"numero": pedido["numero"],
"cliente": pedido["cliente"]["nombre"],
"total": total,
}
def formatear_mensaje(self, resumen):
return (
"Pedido "
+ resumen["numero"]
+ " | Cliente: "
+ resumen["cliente"]
+ " | Total: "
+ str(resumen["total"])
)
def guardar(self, resumen):
self.historial.append(resumen)
def procesar(self, pedido):
resumen = self.crear_resumen(pedido)
self.guardar(resumen)
return self.formatear_mensaje(resumen)
La clase calcula, crea resumen, formatea mensajes, guarda historial y coordina el flujo.
Crea tests/test_servicio_pedidos.py:
from servicio_pedidos import ServicioPedidos
def test_procesa_pedido_vip_online_y_guarda_historial():
servicio = ServicioPedidos()
pedido = {
"numero": "P-100",
"canal": "online",
"cliente": {"nombre": "Ana", "tipo": "vip"},
"items": [
{"precio": 1000, "cantidad": 2},
{"precio": 500, "cantidad": 1},
],
}
mensaje = servicio.procesar(pedido)
assert mensaje == "Pedido P-100 | Cliente: Ana | Total: 2750.0"
assert servicio.historial == [
{"numero": "P-100", "cliente": "Ana", "total": 2750.0},
]
Ejecuta:
python -m pytest tests/test_servicio_pedidos.py
Antes de extraer clases, agrupamos mentalmente los métodos:
calcular_total: regla de negocio.crear_resumen: armado de datos de salida.formatear_mensaje: presentación.guardar y historial: almacenamiento en memoria.procesar: coordinación.Esta clasificación orienta qué clases pequeñas podrían existir.
Primero movemos el cálculo a una clase específica:
class CalculadoraPedido:
def calcular_total(self, pedido):
total = 0
for item in pedido["items"]:
total = total + item["precio"] * item["cantidad"]
if pedido["cliente"]["tipo"] == "vip":
total = total * 0.9
if pedido["canal"] == "online":
total = total + 500
return round(total, 2)
La clase original puede delegar:
class ServicioPedidos:
def __init__(self):
self.historial = []
self.calculadora = CalculadoraPedido()
def calcular_total(self, pedido):
return self.calculadora.calcular_total(pedido)
Ejecuta las pruebas después del cambio.
El formato del mensaje puede moverse a otra clase:
class FormateadorPedido:
def formatear_mensaje(self, resumen):
return (
"Pedido "
+ resumen["numero"]
+ " | Cliente: "
+ resumen["cliente"]
+ " | Total: "
+ str(resumen["total"])
)
La clase original delega:
def formatear_mensaje(self, resumen):
return self.formateador.formatear_mensaje(resumen)
Si mañana cambia el formato, la regla de cálculo no se toca.
El historial representa almacenamiento en memoria. Podemos darle una clase propia:
class RepositorioPedidos:
def __init__(self):
self.historial = []
def guardar(self, resumen):
self.historial.append(resumen)
Para mantener compatibilidad con la prueba anterior, la fachada puede exponer una propiedad historial.
ServicioPedidos puede quedar como coordinador de las piezas:
class ServicioPedidos:
def __init__(self):
self.calculadora = CalculadoraPedido()
self.formateador = FormateadorPedido()
self.repositorio = RepositorioPedidos()
@property
def historial(self):
return self.repositorio.historial
def calcular_total(self, pedido):
return self.calculadora.calcular_total(pedido)
def crear_resumen(self, pedido):
total = self.calcular_total(pedido)
return {
"numero": pedido["numero"],
"cliente": pedido["cliente"]["nombre"],
"total": total,
}
def formatear_mensaje(self, resumen):
return self.formateador.formatear_mensaje(resumen)
def guardar(self, resumen):
self.repositorio.guardar(resumen)
def procesar(self, pedido):
resumen = self.crear_resumen(pedido)
self.guardar(resumen)
return self.formatear_mensaje(resumen)
La interfaz pública se conserva, pero internamente hay responsabilidades separadas.
Si la creación del resumen empieza a crecer, también puede moverse:
class CreadorResumenPedido:
def __init__(self, calculadora):
self.calculadora = calculadora
def crear_resumen(self, pedido):
total = self.calculadora.calcular_total(pedido)
return {
"numero": pedido["numero"],
"cliente": pedido["cliente"]["nombre"],
"total": total,
}
No siempre hace falta llegar a este nivel. Se justifica cuando el resumen tiene reglas propias o cambia por motivos distintos.
Una versión razonable puede ser:
class CalculadoraPedido:
def calcular_total(self, pedido):
total = 0
for item in pedido["items"]:
total = total + item["precio"] * item["cantidad"]
if pedido["cliente"]["tipo"] == "vip":
total = total * 0.9
if pedido["canal"] == "online":
total = total + 500
return round(total, 2)
class FormateadorPedido:
def formatear_mensaje(self, resumen):
return (
"Pedido "
+ resumen["numero"]
+ " | Cliente: "
+ resumen["cliente"]
+ " | Total: "
+ str(resumen["total"])
)
class RepositorioPedidos:
def __init__(self):
self.historial = []
def guardar(self, resumen):
self.historial.append(resumen)
class ServicioPedidos:
def __init__(self):
self.calculadora = CalculadoraPedido()
self.formateador = FormateadorPedido()
self.repositorio = RepositorioPedidos()
@property
def historial(self):
return self.repositorio.historial
def crear_resumen(self, pedido):
total = self.calculadora.calcular_total(pedido)
return {
"numero": pedido["numero"],
"cliente": pedido["cliente"]["nombre"],
"total": total,
}
def procesar(self, pedido):
resumen = self.crear_resumen(pedido)
self.repositorio.guardar(resumen)
return self.formateador.formatear_mensaje(resumen)
Ejecuta python -m pytest tests/test_servicio_pedidos.py para confirmar que el comportamiento se mantiene.
Ahora podemos probar la calculadora sin historial ni formato:
from servicio_pedidos import CalculadoraPedido
def test_calculadora_pedido_vip_online():
pedido = {
"numero": "P-100",
"canal": "online",
"cliente": {"nombre": "Ana", "tipo": "vip"},
"items": [
{"precio": 1000, "cantidad": 2},
{"precio": 500, "cantidad": 1},
],
}
assert CalculadoraPedido().calcular_total(pedido) == 2750.0
Las pruebas pequeñas suelen ser más directas y más fáciles de diagnosticar.
En este ejemplo usamos composición: ServicioPedidos tiene una calculadora, un formateador y un repositorio. No usamos herencia.
La composición suele ser más flexible para refactoring porque permite reemplazar una parte sin crear jerarquías rígidas. La herencia tiene sentido cuando hay una relación clara de especialización, no solo para compartir código.
Dividir una clase grande no significa crear una clase por cada método. Conviene detenerse cuando cada pieza tiene una responsabilidad clara y el costo de navegar el código sigue siendo razonable.
Si una nueva clase solo tiene un método trivial y no representa un concepto, tal vez todavía no hace falta extraerla.
Crea el archivo src/servicio_inscripciones.py:
class ServicioInscripciones:
def __init__(self):
self.registros = []
def calcular_total(self, inscripcion):
total = inscripcion["curso"]["precio"]
if inscripcion["alumno"]["tipo"] == "becado":
total = total * 0.5
if inscripcion["modalidad"] == "presencial":
total = total + 3000
return round(total, 2)
def crear_registro(self, inscripcion):
return {
"alumno": inscripcion["alumno"]["nombre"],
"curso": inscripcion["curso"]["nombre"],
"total": self.calcular_total(inscripcion),
}
def formatear_mensaje(self, registro):
return registro["alumno"] + " inscripto en " + registro["curso"]
def procesar(self, inscripcion):
registro = self.crear_registro(inscripcion)
self.registros.append(registro)
return self.formatear_mensaje(registro)
Realiza estas tareas:
procesar.registros.python -m pytest después de cada paso.Antes de continuar, verifica que puedes explicar estos puntos:
En este tema dividimos una clase grande en responsabilidades pequeñas: cálculo, formato, almacenamiento y coordinación. La clase original quedó como una fachada más simple, mientras que las reglas específicas se movieron a piezas más cohesivas y fáciles de probar.
En el próximo tema usaremos dataclasses y type hints para aclarar estructuras de datos.