3. DI explicite¶
Règle. Pas de conteneur DI. Les dépendances sont passées en constructeur ou via une factory de closures. Si un objet
Xa besoin d'un objetY, c'est écrit noir sur blanc.
Pourquoi¶
Les conteneurs DI (Dependency Injector, Punq, etc.) cachent le graphe d'objets dans une configuration externe. Symptômes :
- Difficulté à savoir qui a besoin de quoi sans dérouler le conteneur.
- Erreurs de câblage à l'exécution, pas à la vérification de types.
- Tests qui doivent monter un mini-conteneur, ou pire, monkeypatcher.
MIC choisit une approche plus simple : factory + closures.
Le motif : factory qui retourne un tuple de routes¶
def build_mail_v1_routes(
*,
enqueue_mail_controller: EnqueueMailController, # (1)
) -> tuple[HttpRouteSpec, ...]:
def _enqueue(*, payload: EnqueueMailPayload) -> HttpResponseSpec:
response = handle_enqueue_mail( # (2)
payload, controller=enqueue_mail_controller,
)
return HttpResponseSpec(status_code=201, body=wrap_http_success(response))
return (HttpRouteSpec(..., handler=_enqueue),)
- La factory prend ses dépendances en argument explicite (ici les controllers déjà construits).
- Le handler capture la dépendance via la closure et l'utilise au moment voulu.
Le wiring final¶
db = SqlDataStorage(url=settings.database_url)
db.prepare()
mail_repo = MailRepository(session_factory=db.session) # repository ← session
routes = (
*build_mail_v1_routes(
enqueue_mail_controller=EnqueueMailController(repository=mail_repo), # controller ← repository
),
*build_audit_v1_routes(...),
)
app = FastAPI()
app.include_router(build_fastapi_router(routes=routes))
L'assemblage se lit de haut en bas : session → repository → controller →
routes. Pas de magie.
Les cas d'usage — même règle¶
class EnqueueMailController:
def __init__(self, *, repository: MailRepository) -> None:
self._repository = repository
def execute(self, *, recipient: str, ...) -> EnqueueMailResult:
... # validation, puis self._repository.enqueue_mail(...)
class MailRepository:
def __init__(
self,
*,
session_factory: Callable[[], AbstractContextManager[Session]],
) -> None:
self._session_factory = session_factory
Pas de @inject, pas de Depends(). Chaque constructeur prend ses
dépendances. Les tests injectent ce qu'ils veulent.
Les tests deviennent triviaux¶
def test_enqueue_creates_pending_job(database):
controller = EnqueueMailController(
repository=MailRepository(session_factory=database.session),
)
result = controller.execute(recipient="a@b.c", subject="s", body="b")
assert result.status == "pending"
Aucune fixture magique, aucun monkeypatch. La fixture database est
définie en 5 lignes dans conftest.py.