Módulo 5 · Arquitectura y código mantenible

Lección 24 — SOLID e inyección de dependencias

Separación por capas y dependencias que apuntan en la dirección correcta.

Publicada
En esta lección
  1. Objetivos
  2. 1. SOLID en TicketFlow (cada letra, un ejemplo tuyo)
  3. 2. Las capas de TicketFlow
  4. 3. Inyección en Python (sin contenedores de gore)
  5. 4. Señales de deuda de arquitectura (las cuelas que lees en review)
  6. Autoevaluación

Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — apertura del módulo de arquitectura Prerrequisito: Lección 23 — Secretos y RGPD


Objetivos

  1. Explicar cada principio SOLID con un ejemplo de TU proyecto (no con Shape/Circle de tutorial).
  2. Separar capas: vista (HTTP) → servicio (dominio) → modelos (persistencia), y saber qué va en cada una.
  3. Aplicar inyección de dependencias en Python sin ceremonia (duck typing + protocolos).

1. SOLID en TicketFlow (cada letra, un ejemplo tuyo)

S — Single Responsibility: reservar() (10) reserva; no envía emails ni calcula impuestos. La vista traduce HTTP; el servicio ejecuta dominio; el modelo persiste y mantiene invariantes. Cuando una función necesita "y además", es la señal.

O — Open/Closed: añadir un nuevo método de pago (tarjeta regalo de la 08, pasarela B) sin tocar el código existente: un PaymentMethod con interfaz común (charge(amount, intent)) y registro de implementaciones, en vez de un if method == "card" elif... que crece para siempre. Extender = añadir clase; modificar = no.

L — Liskov: toda implementación de PaymentGateway respeta el contrato: charge devuelve o lanza, nunca devuelve None silencioso "a veces". Si una pasarela simulada devuelve {"ok": True} y la real {"status": "ok"}, el código que las usa se rompe — la sustituibilidad es el contrato exacto, incluidas las excepciones.

I — Interface Segregation: la vista de disponibilidad no necesita un "repository que también envía emails". Interfaces pequeñas por consumidor: AvailabilityReader (lee), ReservationWriter (escribe) — los clientes no dependen de métodos que no usan.

D — Dependency Inversion: el servicio de reserva no importa psycopg ni Redis directamente: recibe clock, payment_gateway, cache por parámetro/constructor. El dominio define qué necesita; la infraestructura lo cumple.

2. Las capas de TicketFlow

views/serializers  →  HTTP: parse, auth, status codes (13/26)
services/          →  DOMINIO: reglas, transacciones (10), invariantes
models/            →  PERSISTENCIA: tablas, constraints, máquinas de estado (00b)

Reglas de frontera: la vista NO consulta el ORM en loops (eso es lógica de dominio); el servicio NO construye Response ni lee request (eso es HTTP); el modelo NO importa Redis ni llama APIs (eso es infraestructura). La prueba del algodón: ¿puedes llamar reservar(user, seats) desde un comando de management, un worker y un test sin Django request? Si sí, las capas están bien.

3. Inyección en Python (sin contenedores de gore)

Python no necesita Spring: las dependencias entran por parámetro con defaults, y el duck typing + typing.Protocol documentan el contrato:

python
class Clock(Protocol):
    def now(self) -> datetime: ...

class SystemClock:
    def now(self) -> datetime:
        return timezone.now()

def reservar(user, seats, *, clock: Clock = SystemClock(), expires_in: timedelta = timedelta(minutes=10)):
    expires_at = clock.now() + expires_in     # el test inyecta FakeClock y controla el tiempo

El test de la 10 con expires_at fijo deja de depender de timezone.now(); el test de expiración del job (29) avanza el reloj virtual. La DI aquí es: constructor + default de producción + fake en test. Un contenedor solo cuando el grafo crece de verdad (no es este proyecto).

4. Señales de deuda de arquitectura (las cuelas que lees en review)

  • Un utils.py de 900 líneas (el cajón de sastre): dividir por dominio.
  • Import de requests/redis dentro de services/reservations.py: dependencia de infraestructura dura → inyectar.
  • if user.role == "STAFF" repetido en 12 lugares: la regla viva en un lugar (permissions.py/servicios).
  • Vista de 150 líneas: mezcla HTTP+dominio: extraer servicio.
  • Modelos sin comportamiento (anémicos) con toda la lógica en servicios: válido, pero las invariantes de esquema (00b) deben seguir en el modelo — el equilibrio del proyecto: máquinas de estado en el modelo, flujos en servicios.

Autoevaluación

  1. Tu vista de reserva tiene 150 líneas con ORM en loops: ¿qué extraes y a qué capa lleva cada pieza?
  2. ¿Qué problema concreto resuelve la L en PaymentGateway y cómo se manifiesta su violación en tests?
  3. ¿Por qué Clock es la primera dependencia que conviene inyectar en un sistema de reservas?
  4. ¿Cuándo un modelo "anémico" es señal de problema y cuándo es elección legítima en Django?
  5. ¿Qué prueba del algodón aplicas para saber si tus capas están bien separadas?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.