01. La règle de dépendance
La seule règle qui compte — et comment la rendre vérifiable plutôt que déclarative.
import
pointent vers l'intérieur, vers le cœur métier. Tout le reste du cours en
découle. Elle est vérifiable mécaniquement, en CI, par import-linter.
L'architecture hexagonale — publiée par Alistair Cockburn en 2005 sous le nom ports & adapters — répond au problème du chapitre 00 avec une règle unique.
La règle#
Les dépendances du code pointent toujours vers l'intérieur. Le cœur ne connaît aucune implémentation concrète du monde extérieur.
Attention à une confusion fréquente : « le cœur ne connaît rien de l'extérieur » ne veut pas dire qu'il ignore tout besoin externe. Le domaine déclare ce dont il a besoin (« je dois pouvoir sauvegarder une tâche ») sous forme d'interfaces — les ports — sans jamais savoir qui les réalise. C'est de l'inversion de dépendance : le besoin est possédé par celui qui l'exprime, pas par celui qui le satisfait.
L'analogie de la prise électrique tient bien : ton appartement définit un standard de prise (le port). Il ne sait pas si tu y brancheras une lampe ou un aspirateur (les adaptateurs). Et surtout, ce n'est pas l'aspirateur qui dicte la forme de la prise.
Les flèches représentent les import. À l’exécution, le use case appelle un adaptateur à travers son port. Dans le code, c’est l’adaptateur de persistance qui dépend de l’interface du cœur ; le serveur PostgreSQL ne dépend évidemment pas de notre code Python.
Les quatre couches de ce projet#
| Couche | Rôle | Dépend de |
|---|---|---|
domain/ | Le cœur : entités, value objects, ports métier, exceptions | rien (aucun framework) |
application/ | Use cases, DTO, ports techniques | domain/ |
infrastructure/ | Adaptateurs driven : SQLAlchemy, Valkey, SMTP, RabbitMQ, config | application/, domain/ |
presentation/ | Adaptateurs driving : API HTTP, CLI, worker | tout le reste |
Driving vs driven : les deux côtés de l'hexagone#
- Un adaptateur driving (pilotant) déclenche l'application.
Ce projet en a trois : l'API HTTP, le CLI
(
presentation/cli/) et le worker RabbitMQ (presentation/worker/). - Un adaptateur driven (piloté) est appelé par l'application : PostgreSQL, Valkey, SMTP, le broker.
Le point clé : un use case ne connaît ni l'adaptateur qui l'appelle, ni celui
qui le sert. C'est exactement pour ça que CreateUser est réutilisé
tel quel par l'API et par le CLI de ce projet, sans une ligne
d'adaptation.
Un même outil peut d'ailleurs apparaître des deux côtés : RabbitMQ est
driven quand ShareTask y publie un message, et
driving quand le worker consomme ce message pour déclencher
NotifyTaskShared.
Comment on la vérifie ici#
Une règle d'architecture qui n'est pas vérifiée automatiquement se dégrade en
quelques mois. La vérification naïve est un grep :
grep -rE "fastapi|sqlalchemy|pydantic" src/task_manager/domain/ # → doit être videC'est un bon réflexe de contrôle rapide, mais ce n'est pas la
source de vérité de ce projet : un grep ne voit pas les imports
transitifs (un module du domaine qui importe un module « pur »
qui, lui, importe SQLAlchemy) et produit des faux positifs sur des commentaires.
La vraie garantie est import-linter, configuré dans
pyproject.toml ([tool.importlinter]), qui analyse le
graphe d'imports réel :
make imports # ou : uv run lint-importsDeux contrats sont déclarés :
layers— l'empilementpresentation → infrastructure → application → domain. Interdit par exemple qu'application/importeinfrastructure/.forbidden— aucun defastapi,sqlalchemy,pydantic,pydantic_settings,starlettene doit être atteignable depuistask_manager.domain, même indirectement.
Ces contrats disent « domaine sans framework », pas « domaine sans aucune
dépendance externe ». Le second est plus strict, et ce projet ne le tient pas :
domain/user/ importe email_validator. C'est un choix
assumé, pas un oubli — l'exercice 1 ci-dessous porte précisément sur cette
frontière.
En cas de violation, l'outil affiche la chaîne d'imports fautive avec le numéro de ligne :
Architecture hexagonale (dépendances vers l'intérieur) KEPT
Domaine sans framework BROKEN
Contracts: 1 kept, 1 broken.
----------------
Broken contracts
----------------
Domaine sans framework
----------------------
task_manager.domain is not allowed to import sqlalchemy:
- task_manager.domain.task.entities -> sqlalchemy (l.10)import-linter tourne dans make lint et dans les hooks pre-commit lorsqu’ils sont installés. Le seul workflow GitHub versionné publie le site ; aucune CI backend ne lance encore ces contrôles. Les brancher en CI reste nécessaire pour protéger les contributions qui contournent les hooks locaux.
| Concept | Ce que dit le principe | Ce que fait ce projet |
|---|---|---|
| Vérification de l'archi | La règle doit être testable, pas seulement documentée | import-linter (2 contrats) en make lint + hooks pre-commit |
| Nombre de couches | Non prescrit (dedans / dehors) | 4 couches, héritées de la Clean Architecture |
Ce que ça t'apporte concrètement#
- Testabilité — le domaine se teste sans base ni HTTP,
en millisecondes (les tests de
tests/unit/domain/, quasi instantanés). - Remplaçabilité — ce template est passé de SQLite à
PostgreSQL : seuls la config, le moteur SQLAlchemy et les tests ont changé.
Zéro ligne de
domain/ni d'application/. - Réutilisation des cas d'usage —
CreateUsersert l'API et le CLI. - Lisibilité — les règles métier se lisent sans la technique autour.
À retenir#
- Une seule règle : les dépendances pointent vers l'intérieur.
- Le cœur ne connaît pas les implémentations, mais il déclare ses besoins via des ports (inversion de dépendance).
- « Port » = interface, « adaptateur » = implémentation. Driving = pilote, driven = piloté. Un même outil peut être les deux.
- Une règle d'architecture non automatisée se dégrade : ici, import-linter.
À toi de jouer#
domain/user/value_objects.pyimporteemail_validator, une bibliothèque tierce. Est-ce une violation de la règle de dépendance ? Justifie — et regarde le contratforbiddendepyproject.tomlpour vérifier.- Un collègue place
TaskRepository(l'interface) dansinfrastructure/et fait importer ce module parapplication/. Quel contrat import-linter casse, et quelle capacité concrète perd-on ?
→ Corrigés