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. Ejercicio 1 — Vista operada
  2. Ejercicio 2 — Clock
  3. Ejercicio 3 — L
  4. Ejercicio 4 — O
  5. Ejercicio 5 — I
  6. Resumen del profesor

Ejercicio 1 — Vista operada

After esperado (la vista entera):

python
@api_view(["POST"])
def reserve_view(request):
    serializer = ReserveInputSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    try:
        reservation = reservar(request.user, **serializer.validated_data)
    except DomainError as exc:
        return problem(exc)                     # 13
    return ReservationOut(reservation, status=201)

~15 líneas vs 150: el HTTP es traducción, el dominio vive en el servicio, y manage.py shell >>> reservar(u,...) funciona sin request — la prueba del algodón. Si falló: queda algún request/Response escondido en el servicio (retíralo: inyecta datos, no HTTP).

Ejercicio 2 — Clock

python
class FakeClock:
    def __init__(self, fixed: datetime): self._fixed = fixed
    def now(self) -> datetime: return self._fixed

def test_reserva_expira_diez_minutos_despues():
    clock = FakeClock(datetime(2027, 3, 1, 23, 59, tzinfo=dt.UTC))
    r = reservar(user=self.buyer, event_uuid=..., seat_refs=[...], clock=clock)
    self.assertEqual(r.expires_at, datetime(2027, 3, 2, 0, 9, tzinfo=dt.UTC))

Sin sleeps ni freezegun-patch: la dependencia entra por parámetro y el test es determinista. El job de la 29 usará el mismo Clock para "ahora" — un solo lugar donde el tiempo es negociable en tests.

Ejercicio 3 — L

  1. El test que la caza:
python
def test_gateway_siempre_responde_o_lanza(self):
    with self.assertRaises((GatewayError, ChargeResult)):
        self.broken_gateway.charge(Amount(50), intent)

Si la implementación rota devuelve None, assertRaises falla (no lanzó) y el tipo no coincide: la sustituibilidad rota en tests antes que en producción — ese es el valor de L con Protocolos + tests de contrato por implementación.

  1. Contrato docstring: "Devuelve ChargeResult(status=SUCCEEDED|PENDING). Lanza GatewayError en fallo temporal y DomainConflict en rechazo de negocio. Garantiza idempotencia por intent.id (14)". Con typing estricto (-> ChargeResult) mypy caza el None en estático: docstring + types = contrato honesto.

Ejercicio 4 — O

python
PAYMENT_METHODS: dict[str, type[PaymentMethod]] = {}

def register(name: str):
    def deco(cls):
        PAYMENT_METHODS[name] = cls
        return cls
    return deco

@register("gift_card")
class GiftCardMethod(PaymentMethod): ...

@register("cash_on_delivery")
class CodMethod(PaymentMethod): ...

Añadir "crypto" = 1 archivo nuevo con su clase + registro; el servicio itera PAYMENT_METHODS[name] sin conocer implementaciones. El if/elif habría tocado N lugares (violación O y riesgo de merge). Nota fina: el registro explícito evita los auto-imports mágicos que confunden.

Ejercicio 5 — I

  1. Primera tentativa: una interfaz "ReservationRepository" con 14 métodos. El endpoint público usa 1 (leer disponibilidad); el checkout, 3. Interfaces por consumidor: AvailabilityReader.read(event_id) -> list[SeatFree] y ReservationWriter.create(...) — nadie depende de métodos que no usa; los fakes de test son diminutos.
  2. El utils.py se divide por dominio; la regla preventiva: "utils es solo funciones puras sin estado; si necesita config o I/O, es un servicio".

Resumen del profesor

  • Las capas se prueban con la llamada sin request; los principios, con tests que fallan cuando los violas.
  • Clock, gateway y cache: las primeras dependencias que valen inyectar (tiempo, dinero, red = lo no determinista).
  • Protocol + default de producción: DI en Python sin contenedores; el fake de test es la documentación viviente.
  • O se demuestra añadiendo un método de pago sin tocar el servicio: 1 archivo nuevo, 0 modificados.

Después: Lección 25 — Patrones de servicio.