Clean Architecture : construire un logiciel qui survit à son framework
La Clean Architecture, ce n'est pas une histoire de dossiers ou de diagrammes, c'est une seule règle : les dépendances pointent vers l'intérieur. Voici ce que ça change vraiment dans un vrai codebase.
La Clean Architecture de Robert C. Martin a popularisé un schéma de cercles concentriques, mais le schéma n'est pas l'essentiel. L'essentiel est une seule règle qui, appliquée avec constance, garde votre logique métier utilisable longtemps après que le framework dans lequel vous l'avez construite ait été remplacé. Elle appartient à la même famille que l'architecture hexagonale (ports et adapters) et l'architecture en oignon : même idée centrale, vocabulaire différent.
La règle de dépendance
Les dépendances du code source ne peuvent pointer que vers l'intérieur. Les couches externes, le framework web, la base de données, l'UI, dépendent des couches internes : use cases et entities. L'inverse n'est jamais autorisé. Un use case ne doit jamais importer un modèle Eloquent, un contrôleur Symfony ou un client HTTP directement. Quand une couche interne a réellement besoin de quelque chose de l'extérieur, par exemple enregistrer une commande, elle déclare une interface qu'elle possède, et la couche externe l'implémente. C'est le principe d'inversion des dépendances, et c'est ce qui rend la règle applicable plutôt que théorique.
Les couches
| Couche | Responsabilité | Exemples | Connaît |
|---|---|---|---|
| Entities | Règles métier valables pour toute l'entreprise | Order, Money, Invoice | Rien en dehors du langage pur |
| Use cases | Workflows spécifiques à l'application | PlaceOrder, CancelSubscription | Les entities et les interfaces qu'il définit |
| Interface adapters | Traduire entre les use cases et l'extérieur | Contrôleurs, presenters, implémentations de repository | Les use cases et les entities |
| Frameworks et drivers | Détail technique, la couche volatile | Laravel, PostgreSQL, HTTP, le navigateur | Tout ce qui est vers l'intérieur |
Les noms varient. L'architecture hexagonale parle de ports et adapters, l'architecture en oignon dessine des anneaux plutôt qu'un hexagone, mais la contrainte est identique : les dépendances vont vers le domaine, jamais dans l'autre sens.
Un schéma
+---------------------------------------------------+
| Frameworks & Drivers (Laravel, Postgres, HTTP) |
| +---------------------------------------------+ |
| | Interface Adapters | |
| | (controllers, presenters, repo impls) | |
| | +-------------------------------------+ | |
| | | Use Cases | | |
| | | (PlaceOrder, CancelSubscription) | | |
| | | +---------------------------+ | | |
| | | | Entities | | | |
| | | | (Order, Money, Invoice) | | | |
| | | +---------------------------+ | | |
| | +-------------------------------------+ | |
| +---------------------------------------------+ |
+---------------------------------------------------+
Les dépendances pointent vers l'intérieur, et seulement vers l'intérieur.Lisez-le de l'intérieur vers l'extérieur. Les entities au centre portent des règles qui seraient vraies dans n'importe quelle application de ce métier. Les use cases les enveloppent avec les workflows que cette application réalise. Les interface adapters traduisent ces workflows depuis et vers le monde extérieur. Les frameworks et drivers sont au bord, là où le changement est constant et où le coût de remplacement doit rester faible. Une flèche peut traverser une frontière vers l'intérieur mais jamais vers l'extérieur : une classe externe peut appeler une classe interne, une classe interne ne peut appeler qu'une interface qu'elle a elle-même définie.
Un exemple concret : découpler un use case de Laravel
Un use case doit dépendre d'une interface, pas d'Eloquent. L'implémentation concrète vit dans la couche externe et est liée au runtime, dans un service provider Laravel ou le conteneur Symfony.
// Use case : PHP pur, aucun Eloquent, aucun import de framework
final class PlaceOrder
{
public function __construct(
private OrderRepository $orders, // interface, pas Eloquent
private PaymentGateway $payments, // interface
) {}
public function execute(PlaceOrderRequest $request): Order
{
$order = Order::create($request->items, $request->customerId);
$this->payments->charge($order->total(), $request->paymentToken);
$this->orders->save($order);
return $order;
}
}// Couche domaine : le contrat, possédé par l'intérieur
interface OrderRepository
{
public function save(Order $order): void;
public function find(string $id): ?Order;
}// Couche infrastructure : implémente le contrat avec Eloquent
final class EloquentOrderRepository implements OrderRepository
{
public function save(Order $order): void
{
OrderModel::updateOrCreate(['id' => $order->id()], $order->toArray());
}
public function find(string $id): ?Order
{
$model = OrderModel::find($id);
return $model ? Order::fromModel($model) : null;
}
}
// Lié une fois dans un service provider, jamais référencé par le use case
$this->app->bind(OrderRepository::class, EloquentOrderRepository::class);Le use case ne nomme jamais Eloquent. Il travaille contre OrderRepository, et le conteneur décide quelle implémentation injecter. Remplacer Postgres par un autre store, ou Eloquent par Doctrine, est un changement dans une seule classe de la couche externe.
Tester sans base de données
Parce que le use case dépend d'une interface, un test peut lui passer un faux qui garde les commandes dans un tableau. Aucune migration, aucun serveur HTTP, aucune base de test à réinitialiser entre les exécutions.
final class InMemoryOrderRepository implements OrderRepository
{
private array $orders = [];
public function save(Order $order): void
{
$this->orders[$order->id()] = $order;
}
public function find(string $id): ?Order
{
return $this->orders[$id] ?? null;
}
}
// Le test : aucun framework, quelques millisecondes
$useCase = new PlaceOrder(new InMemoryOrderRepository(), new FakePaymentGateway());
$order = $useCase->execute($request);
assert($order->total()->equals(Money::eur(4200)));Ce que ça apporte
- ✓Testabilité : les règles métier sont testées en mémoire, en quelques millisecondes, sans infrastructure.
- ✓Infrastructure interchangeable : changer d'ORM, de base de données ou de mécanisme de livraison sans toucher une règle métier.
- ✓Montées de version localisées : un saut de version majeure du framework touche la couche externe, pas le domaine.
- ✓Logique métier lisible : les règles vivent au même endroit, dans un langage qu'un expert métier reconnaîtrait.
- ✓Travail en parallèle : un développeur sur le domaine, un autre sur l'infrastructure, contre une interface partagée.
Ce que ça coûte
- ✓Plus de fichiers et plus d'indirection : une interface et une implémentation là où un appel direct aurait suffi.
- ✓Du mapping : les objets du domaine et les modèles ORM sont séparés, donc vous traduisez entre les deux.
- ✓Coût d'onboarding : un développeur nouveau sur la base de code a plus de concepts à apprendre avant d'être productif.
- ✓Risque de sur-ingénierie : sur un écran CRUD simple, les couches coûtent plus qu'elles ne rapportent.
Clean Architecture vs architecture en couches classique
Une appli en couches classique va du contrôleur au service au repository à la base, avec des dépendances qui pointent vers le bas. Ça se ressemble, mais la base façonne toujours le code au-dessus : les entities sont en général des modèles ORM, donc un changement de schéma remonte. La Clean Architecture inverse la dépendance à la base. Le domaine définit l'interface du repository, et l'implémentation base de données dépend du domaine, pas l'inverse. La différence est le sens des flèches, pas le nombre de couches.
En pratique, la plupart des équipes se situent entre les deux. Une version pragmatique garde les entities et les use cases en classes simples, met les interfaces de repository et de gateway dans le domaine, et laisse les contrôleurs être de fines classes Laravel ou Symfony qui appellent un use case et formatent le résultat. Vous obtenez la testabilité et l'isolation sans un plan rigide en quatre dossiers ni un mapper pour chaque modèle.
Quand l'utiliser
Elle est rentable quand la logique métier est complexe, censée durer des années, ou doit être testée et évoluer indépendamment de la façon dont elle est livrée. Elle est superflue pour un back-office CRUD, un prototype, ou un projet à courte durée de vie. L'adoption partielle est légitime : vous pouvez garder la règle unique sans toute la structure de dossiers. La décision est rarement tout ou rien, et elle peut changer à mesure qu'un module gagne en importance.
Si vous ne retenez qu'une chose de la Clean Architecture, retenez celle-ci : ne laissez pas les classes du framework s'infiltrer dans votre logique métier. Cette seule discipline évite l'essentiel des douleurs que l'approche complète est censée résoudre, à un coût quasi nul.
FAQ
La Clean Architecture en termes simples ?
C'est une façon d'organiser le code pour que les règles métier ne dépendent pas du framework, de la base de données ni de l'UI. Ces éléments sont traités comme des détails, branchés depuis l'extérieur. Une règle garde le cap : les dépendances du code source ne pointent que vers l'intérieur, vers le domaine.
Clean Architecture et architecture hexagonale, est-ce pareil ?
Elles sont très proches. L'architecture hexagonale (ports et adapters), l'architecture en oignon et la Clean Architecture partagent toutes l'inversion des dépendances vers le domaine. La Clean Architecture ajoute un vocabulaire de couches précis, entities, use cases, interface adapters, frameworks, et le schéma concentrique.
Ça marche avec Laravel ou Symfony ?
Oui. Le framework devient une couche externe. Sous Laravel, vous liez les interfaces aux implémentations dans un service provider ; sous Symfony, vous les câblez dans le conteneur de services. Les modèles Eloquent ou Doctrine deviennent un détail d'infrastructure caché derrière une interface de repository.
Est-ce surdimensionné pour un petit projet ?
Souvent, oui. Pour une appli CRUD ou un projet à courte durée de vie, les fichiers et l'indirection en plus coûtent davantage qu'ils ne rapportent. Gardez la règle unique, aucune classe de framework dans la logique métier, et laissez tomber le reste.
Même sans adopter toute la structure, la discipline en vaut la peine. Gardez vos règles métier en PHP simple, mettez une interface entre elles et tout ce qui est technique, et votre code survivra à la prochaine migration de framework avec bien moins de reprise.
Besoin d'aide sur ce sujet ? Développement Full Stack
Découvrir ce service →Articles liés
Clean Architecture vs Hexagonale vs DDD : les différences
ArchitectureDomain-Driven Design : modéliser le métier, pas la base de données
ArchitectureArchitecture microservices : les principes qui comptent vraiment
ArchitectureLes erreurs classiques d'architecture d'API REST (et comment les corriger)
