Module 5 · Architecture and maintainable code

Lesson 24 — SOLID and dependency injection

Layer separation and dependencies pointing in the right direction.

Published
In this lesson
  1. Exercise 1 — The operated view
  2. Exercise 2 — Clock
  3. Exercise 3 — L
  4. Exercise 4 — O
  5. Exercise 5 — I
  6. Professor's summary

Exercise 1 — The operated view

Expected after (the whole view):

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 lines vs 150: HTTP is translation, the domain lives in the service, and manage.py shell >>> reservar(u,...) works without a request — the cotton test. If it failed: some request/Response is still hiding in the service (remove it: inject data, not HTTP).

Exercise 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))

No sleeps or freezegun patches: the dependency enters by parameter and the test is deterministic. Lesson 29's job will use the same Clock for "now" — a single place where time is negotiable in tests.

Exercise 3 — L

  1. The test that catches it:
python
def test_gateway_siempre_responde_o_lanza(self):
    with self.assertRaises((GatewayError, ChargeResult)):
        self.broken_gateway.charge(Amount(50), intent)

If the broken implementation returns None, assertRaises fails (nothing was raised) and the type doesn't match: substitutability breaks in tests before production — that is the value of L with Protocols + contract tests per implementation.

  1. Docstring contract: "Returns ChargeResult(status=SUCCEEDED|PENDING). Raises GatewayError on temporary failure and DomainConflict on business rejection. Guarantees idempotency by intent.id (14)". With strict typing (-> ChargeResult) mypy catches the None statically: docstring + types = an honest contract.

Exercise 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): ...

Adding "crypto" = 1 new file with its class + registration; the service iterates PAYMENT_METHODS[name] without knowing implementations. The if/elif would have touched N places (an O violation and a merge risk). Fine note: the explicit registry avoids the magic auto-imports that confuse people.

Exercise 5 — I

  1. First attempt: a "ReservationRepository" interface with 14 methods. The public endpoint uses 1 (read availability); the checkout, 3. Interfaces per consumer: AvailabilityReader.read(event_id) -> list[SeatFree] and ReservationWriter.create(...) — nobody depends on methods they don't use; test fakes are tiny.
  2. utils.py is split by domain; the preventive rule: "utils is only pure stateless functions; if it needs config or I/O, it is a service".

Professor's summary

  • Layers are tested with the request-less call; principles, with tests that fail when you violate them.
  • Clock, gateway and cache: the first dependencies worth injecting (time, money, network = the non-deterministic parts).
  • Protocol + production default: DI in Python without containers; the test fake is the living documentation.
  • O is demonstrated by adding a payment method without touching the service: 1 new file, 0 modified.

Next: Lesson 25 — Service patterns.