Los módulos de negocio contienen las reglas importantes del sistema: descuentos, impuestos, validaciones, cálculos y decisiones del dominio. Si esas reglas se mezclan con entrada, salida, archivos o presentación, el código se vuelve más difícil de probar y modificar.
En este tema veremos cómo separar datos, reglas de negocio y presentación en módulos Python simples, sin caer en una arquitectura innecesariamente compleja.
La lógica de negocio representa decisiones propias del problema que estamos resolviendo. En un sistema de ventas, por ejemplo:
Estas reglas deben poder probarse sin depender de la consola, archivos o una interfaz gráfica.
Conviene evitar que una función de negocio haga también:
Este código mezcla lectura, cálculo y presentación:
def procesar_venta(ruta):
with open(ruta, encoding="utf-8") as archivo:
productos = []
for linea in archivo:
precio, cantidad = linea.strip().split(",")
productos.append({"precio": float(precio), "cantidad": int(cantidad)})
total = 0
for producto in productos:
total += producto["precio"] * producto["cantidad"]
if total < 10000:
total += 1500
print(f"Total final: {total:.2f}")
Para probar el cálculo, necesitamos un archivo y debemos capturar salida. La regla de negocio quedó atrapada entre detalles externos.
Primero separamos la lectura y el parseo:
def parsear_producto(linea: str) -> dict[str, float | int]:
precio, cantidad = linea.strip().split(",")
return {"precio": float(precio), "cantidad": int(cantidad)}
def leer_productos(ruta: str):
with open(ruta, encoding="utf-8") as archivo:
return [parsear_producto(linea) for linea in archivo]
Ahora la lectura queda en un lugar específico.
El cálculo puede ser una función independiente:
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500
def calcular_subtotal(productos):
return sum(
producto["precio"] * producto["cantidad"]
for producto in productos
)
def aplicar_envio(total):
if total < LIMITE_ENVIO_GRATIS:
return total + COSTO_ENVIO
return total
def calcular_total(productos):
subtotal = calcular_subtotal(productos)
return aplicar_envio(subtotal)
Estas funciones ya no dependen de archivos ni consola.
La salida puede vivir en otra función:
def formatear_total(total):
return f"Total final: {total:.2f}"
def mostrar_total(total):
print(formatear_total(total))
Podemos probar formatear_total sin imprimir.
Una función principal puede coordinar las partes:
def procesar_venta(ruta):
productos = leer_productos(ruta)
total = calcular_total(productos)
mostrar_total(total)
Esta función orquesta. No contiene detalles de cálculo, parseo ni formato interno.
Una estructura simple puede ser:
src/
|-- modelos.py
|-- ventas.py
|-- entrada.py
|-- presentacion.py
`-- ventas_cli.py
modelos.py: dataclasses o estructuras de datos.ventas.py: reglas de negocio.entrada.py: lectura y parseo de datos externos.presentacion.py: formato de salida.ventas_cli.py: punto de entrada del script.En modelos.py podemos tener:
from dataclasses import dataclass
@dataclass(frozen=True)
class Producto:
precio: float
cantidad: int
@dataclass(frozen=True)
class Venta:
productos: list[Producto]
tipo_cliente: str
pais: str
Estos modelos no leen archivos ni imprimen. Representan datos del dominio.
En ventas.py podemos tener:
from modelos import Producto, Venta
DESCUENTOS = {"vip": 0.15, "regular": 0.05}
IMPUESTOS = {"AR": 0.21, "UY": 0.22}
def calcular_subtotal(productos: list[Producto]) -> float:
return sum(producto.precio * producto.cantidad for producto in productos)
def obtener_descuento(tipo_cliente: str) -> float:
return DESCUENTOS.get(tipo_cliente, 0)
def obtener_impuesto(pais: str) -> float:
return IMPUESTOS.get(pais, 0.19)
def calcular_total_venta(venta: Venta) -> float:
subtotal = calcular_subtotal(venta.productos)
descuento = obtener_descuento(venta.tipo_cliente)
impuesto = obtener_impuesto(venta.pais)
return round(subtotal * (1 - descuento) * (1 + impuesto), 2)
En entrada.py podemos convertir texto en modelos:
from modelos import Producto
def parsear_producto(linea: str) -> Producto:
precio, cantidad = linea.strip().split(",")
return Producto(precio=float(precio), cantidad=int(cantidad))
def leer_productos(ruta: str) -> list[Producto]:
with open(ruta, encoding="utf-8") as archivo:
return [parsear_producto(linea) for linea in archivo]
Si cambia el formato del archivo, el cambio se concentra aquí.
En presentacion.py podemos formatear salida:
def formatear_total(total: float) -> str:
return f"Total final: {total:.2f}"
Esta función no calcula reglas de negocio. Solo decide cómo mostrar el resultado.
En ventas_cli.py coordinamos:
from entrada import leer_productos
from modelos import Venta
from presentacion import formatear_total
from ventas import calcular_total_venta
def main() -> int:
productos = leer_productos("productos.txt")
venta = Venta(productos=productos, tipo_cliente="vip", pais="AR")
total = calcular_total_venta(venta)
print(formatear_total(total))
return 0
if __name__ == "__main__":
raise SystemExit(main())
El punto de entrada une las piezas, pero no contiene las reglas internas.
La separación permite pruebas más simples:
from modelos import Producto, Venta
from ventas import calcular_total_venta
def test_calcular_total_venta():
venta = Venta(
productos=[Producto(precio=1000, cantidad=2)],
tipo_cliente="nuevo",
pais="AR",
)
assert calcular_total_venta(venta) == 2420.0
Esta prueba no necesita archivos ni consola.
Revisa tu código si encuentras:
print.Separa este código en tres responsabilidades: entrada, negocio y presentación.
def ejecutar():
datos = input("Precio y cantidad: ")
precio, cantidad = datos.split(",")
total = float(precio) * int(cantidad)
if total < 10000:
total += 1500
print(f"Total: {total}")
Una solución debería tener una función para parsear entrada, otra para calcular y otra para formatear salida.
En ventas_demo, realiza estas tareas:
print fuera de las funciones de cálculo.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 cómo separar reglas, datos y presentación en módulos Python simples. Esta separación mejora la cohesión, reduce acoplamiento y facilita pruebas enfocadas.
En el próximo tema construiremos una checklist práctica para revisión de código y detección de code smells.