Aller au contenu

Diagramme de clean architecture : couches, règle de dépendance et exemple détaillé

La clean architecture de Robert C. Martin en diagramme à reproduire : les quatre couches, la règle de dépendance et un exemple PlaceOrder avec code et dossiers.

· Fondateur et développeur

Publié le

Quatre paquetages UML côte à côte : frameworks and drivers, interface adapters, use cases et entities. Le Web framework appelle OrderController, qui appelle PlaceOrder, qui crée Order. OrderPresenter et PostgresOrderRepository implémentent des interfaces de la couche des use cases.

La plupart des ingénieurs ont déjà vu l'image de la clean architecture : quatre anneaux colorés avec des flèches pointant vers le centre. Bien moins nombreux sont ceux qui savent dire ce que contient chaque anneau pour une vraie fonctionnalité, dans quel sens les imports doivent pointer ou où se trouve le SQL.

L'écart se voit en code review : un dossier domain qui importe l'ORM, un use case qui renvoie un code de statut HTTP, cinq mappers entre un formulaire et une table.

Cet article transforme les cercles de Robert C. Martin en diagramme de composants pour un seul use case, la passation d'une commande : les couches, la règle de dépendance, le code et les dossiers, les franchissements de frontière, clean face à hexagonal et onion, les erreurs, quand c'est excessif et comment le dessiner.

Les quatre couches

Martin a décrit la clean architecture dans un billet de blog de 2012, The Clean Architecture, puis l'a développée dans son livre de 2017 Clean Architecture. Il l'a présentée comme une synthèse d'idées antérieures, dont l'architecture hexagonale et l'architecture onion. Du centre vers l'extérieur :

Couche Ce qu'elle contient Dans l'exemple Change quand
Entities Règles métier de l'entreprise, réutilisables entre applications Order et sa règle selon laquelle une commande exige au moins une ligne Une règle métier change
Use cases Règles métier de l'application, un workflow chacun PlaceOrder, ses deux interfaces, les modèles de requête et de réponse Le workflow change
Interface adapters Contrôleurs, presenters, gateways et tout le SQL OrderController, OrderPresenter, PostgresOrderRepository L'API ou le schéma change
Frameworks and drivers Outils et code de liaison : framework web, base de données, drivers Le framework HTTP, PostgreSQL, pg, main.ts Vous remplacez un outil

Les cercles sont schématiques ; Martin dit qu'il peut en falloir plus de quatre. La règle qui les relie ne change jamais.

La règle de dépendance

Selon les mots de Martin, « les dépendances du code source ne peuvent pointer que vers l'intérieur ». Le code d'un cercle interne ne mentionne jamais une fonction, une classe ou une variable déclarée dans un cercle externe, ni n'utilise ses formats de données. Pour cet exemple :

  • entities n'importe rien des autres couches.
  • use-cases n'importe que entities : ni framework web, ni ORM, ni driver.
  • Les adapters importent vers l'intérieur, plus la bibliothèque qu'ils adaptent.
  • Seul main.ts voit tout.

La règle porte sur le code source, pas sur les appels à l'exécution : à l'exécution, PlaceOrder appelle vers l'extérieur, le presenter et la base de données. Martin résout cela avec le principe d'inversion des dépendances : le use case appelle une interface qu'il possède, et une classe externe l'implémente. L'appel sort ; la dépendance pointe vers l'intérieur.

Une dépendance pointe bien vers l'extérieur : l'adapter importe le driver de base de données. C'est voulu. Martin place tout le SQL dans la couche des adapters, et aucun code situé plus à l'intérieur ne devrait « rien savoir de la base de données ».

Faites respecter la règle en CI avec le contrôle onionArchitecture() d'ArchUnit sur la JVM, dependency-cruiser pour TypeScript ou le contrat layers d'import-linter pour Python.

Exemple détaillé : passer une commande

Une requête POST /orders suit ce chemin :

  1. Le framework web l'achemine vers OrderController.
  2. Le contrôleur analyse le corps en un PlaceOrderRequest, un objet simple que possède la couche des use cases, crée un OrderPresenter et appelle PlaceOrder.execute().
  3. PlaceOrder laisse Order.place() appliquer les règles métier, enregistre via OrderRepository et transmet un PlaceOrderResponse à PlaceOrderOutput.
  4. OrderPresenter implémente cette interface et construit un view model : statut 201, un en-tête Location et un corps JSON.
  5. Le contrôleur renvoie le view model, et le framework l'écrit.

La couche des use cases, avec les deux interfaces qu'elle possède :

// src/orders/use-cases/place-order.ts
import { Order } from '../entities/order';

export interface OrderRepository {             // implemented by an adapter
  nextId(): Promise<string>;
  save(order: Order): Promise<void>;
}
export interface PlaceOrderOutput {            // implemented by the presenter
  present(response: PlaceOrderResponse): void;
}
export type PlaceOrderRequest = {
  customerId: string;
  lines: { sku: string; quantity: number }[];
};
export type PlaceOrderResponse = { orderId: string; itemCount: number };

export class PlaceOrder {
  constructor(private readonly orders: OrderRepository) {}

  async execute(request: PlaceOrderRequest, output: PlaceOrderOutput): Promise<void> {
    const order = Order.place(await this.orders.nextId(), request.customerId, request.lines);
    await this.orders.save(order);
    output.present({ orderId: order.id, itemCount: order.itemCount() });
  }
}

Elle n'importe que l'entity. Order.place() lève une exception quand une commande n'a aucune ligne ou une quantité inférieure à un, et le fichier de l'entity n'importe rien. L'adapter importe vers l'intérieur, plus le driver :

// src/orders/adapters/postgres/postgres-order-repository.ts
import { randomUUID } from 'node:crypto';
import type { Pool } from 'pg';                         // adapters may import the driver
import type { Order } from '../../entities/order';
import type { OrderRepository } from '../../use-cases/place-order';

export class PostgresOrderRepository implements OrderRepository {
  constructor(private readonly pool: Pool) {}

  async nextId(): Promise<string> { return randomUUID(); }

  async save(order: Order): Promise<void> {
    await this.pool.query('INSERT INTO orders (id, customer_id, lines) VALUES ($1, $2, $3)',
      [order.id, order.customerId, JSON.stringify(order.lines)]);
  }
}

Le composition root est seul à connaître chaque classe concrète :

// src/main.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const controller = new OrderController(new PlaceOrder(new PostgresOrderRepository(pool)));

app.post('/orders', async (req, res) => {
  const view = await controller.post(req.body);        // the presenter's view model
  res.status(view.status).set(view.headers).json(view.body);
});

Un test unitaire de PlaceOrder n'a besoin que d'un repository en mémoire et d'un presenter qui enregistre son entrée : pas de serveur, pas de base de données.

Structure des dossiers

src/
  orders/
    entities/
      order.ts                        Order and its rules
    use-cases/
      place-order.ts                  PlaceOrder, both interfaces, request and response models
    adapters/
      http/
        order-controller.ts
        order-presenter.ts
      postgres/
        postgres-order-repository.ts
  main.ts                             composition root: builds and wires everything

Regroupez par fonctionnalité, puis par couche. L'argument de la screaming architecture de Martin : les dossiers de premier niveau doivent dire ce que fait le système (orders, payments), pas quel framework il utilise. Le chapitre de Simon Brown dans le même livre sépare l'organisation de l'encapsulation : les dossiers organisent le code, tandis que des règles d'accès ou un contrôle de build le font respecter.

Franchir les frontières

Chaque franchissement de frontière transporte un type que possède une couche :

Franchissement Ce qui circule Type possédé par Mécanisme
Framework → contrôleur Requête HTTP Framework Code de liaison dans main.ts
Contrôleur → use case PlaceOrderRequest Use cases Appel direct
Use case → presenter PlaceOrderResponse Use cases Interface PlaceOrderOutput
Use case → base de données Order Entities Interface OrderRepository
Presenter → framework View model Adapters Renvoyé par le contrôleur

Trois habitudes gardent ces franchissements propres :

  • Des modèles de requête et de réponse simples. Martin est direct : « Nous ne voulons pas tricher et passer des entities ou des lignes de base de données. » Le presenter reçoit PlaceOrderResponse, jamais Order, donc renommer un champ de l'entity laisse l'API intacte. Les repositories sont une exception courante : de nombreuses bases de code les laissent accepter des entities, ce qui pointe toujours vers l'intérieur. Les équipes plus strictes font la correspondance avec un enregistrement de persistance dans l'adapter.
  • Une interface à chaque appel vers l'extérieur, possédée par la couche interne et nommée d'après ce dont le use case a besoin, pas d'après la technologie.
  • Un seul composition root. Mark Seemann définit le composition root comme « un emplacement (de préférence) unique dans une application où les modules sont composés ensemble », près du point d'entrée. Martin appelle main « le détail ultime ». Tout le reste reçoit ses dépendances par les constructeurs.

La figure de Martin comporte aussi une interface Use Case Input Port devant l'interactor. Avec un seul contrôleur et une seule implémentation, appeler directement la classe est une simplification courante.

Clean, hexagonale et onion : comparaison

Hexagonale (ports et adapters) Onion Clean
Texte fondateur Alistair Cockburn, article de 2005 Jeffrey Palermo, série de billets de 2008 Robert C. Martin, billet de 2012, livre de 2017
Image Application dans un hexagone, adapters à l'extérieur Anneaux autour du modèle de domaine Quatre cercles concentriques
Structure interne Non prescrite Modèle de domaine, services de domaine, services applicatifs Entities, use cases
Implémentation du repository Adapter secondaire (piloté) Infrastructure, anneau externe Interface adapters
Règle centrale Le code interne ne doit pas fuiter vers l'extérieur ; il passe par des ports Tout couplage pointe vers le centre Les dépendances du code source pointent vers l'intérieur

Les trois placent les règles métier au centre et font dépendre les bords d'elles. Elles diffèrent par la quantité de structure prescrite à l'intérieur : l'article original de Cockburn sépare seulement l'intérieur de l'extérieur, l'onion architecture de Palermo nomme des anneaux autour du modèle de domaine, et la clean architecture ajoute les presenters et les modèles de requête et de réponse. Le diagramme d'architecture hexagonale parcourt un exemple de commande similaire en termes de ports et d'adapters.

Erreurs courantes

  • Des types de framework dans les entities. Des décorateurs d'ORM ou des attributs de sérialisation sur Order lient la couche la plus interne à la plus externe. Faites la correspondance dans l'adapter.
  • Un mapper pour tout. Six copies des mêmes champs pour un formulaire qui enregistre une ligne. Ne faites la correspondance que là où les formes diffèrent.
  • Des couches sans comportement. Des entities réduites à des getters et des setters, le modèle de domaine anémique décrit par Martin Fowler en 2003, et des use cases qui ne font qu'appeler save().
  • Des interfaces partout. Elles appartiennent aux frontières, pas à chaque classe.
  • Du HTTP dans le use case. Les codes de statut appartiennent au presenter.

Quand c'est excessif

Palermo a écrit que l'onion architecture « ne convient pas aux petits sites web », et le chapitre de Martin sur les frontières partielles s'ouvre sur « Les frontières architecturales complètes coûtent cher ».

Situation Meilleure option
Service CRUD avec peu de règles Une application en couches légère ; gardez les quelques règles hors des contrôleurs
Prototype ou outil éphémère Aucune couche
Une dépendance volatile, comme un prestataire de paiement Une interface et un adapter pour elle
Produit durable avec de vraies règles et plusieurs points d'entrée Les quatre couches

Ce chapitre décrit aussi des frontières partielles moins coûteuses ; promouvez-en une quand une seconde implémentation ou un test en a besoin.

Le diagramme de clean architecture

Quatre paquetages UML côte à côte : frameworks and drivers, interface adapters, use cases et entities. Le Web framework appelle OrderController, qui appelle PlaceOrder, qui crée Order. OrderPresenter et PostgresOrderRepository implémentent des interfaces de la couche des use cases.

20 composants · 9 connexions

Ouvrir dans Archboard
Contenu du diagramme
  • Frameworks and drivers
  • Interface adapters
  • Use cases
  • Entities
  • Web framework
  • PostgreSQL
  • OrderPresenter
  • OrderController
  • PostgresOrderRepository
  • PlaceOrderOutput
  • PlaceOrder
  • OrderRepository
  • Order
  • POST /orders
  • execute(request)
  • Order.place()
  • reads view model
  • present(response)
  • save(order)
  • implements
  • SQL via pg driver
Un use case, PlaceOrder, suivi dans les quatre couches de la clean architecture, la plus externe à gauche. Aucune flèche ne pointe vers l'extérieur : le presenter et le repository Postgres implémentent des interfaces des use cases ; le contrôle sort, les dépendances rentrent.

Comment le lire :

  • Quatre paquetages UML, le plus externe à gauche. Aller vers la droite, c'est aller vers l'intérieur.
  • Les flèches pleines sont des appels et des imports. Trois franchissent une frontière vers les entities. Les trois autres restent dans une seule couche : present(response) et save(order) appellent les deux interfaces que possède la couche des use cases.
  • Les flèches en pointillés avec triangles creux signifient implémente. Le presenter et le repository pointent vers l'intérieur sur ces interfaces alors que le contrôle sort.
  • La ligne en points est du SQL via le driver, et elle part d'un adapter.
  • Les notes et le bloc Markdown disent où vit main.ts, ce que l'entity impose et ce que signifie chaque style de ligne.

Dessiner clairement des diagrammes de clean architecture

  • Dessinez un use case, pas les anneaux. Suivez une vraie fonctionnalité avec de vrais noms de classes.
  • Placez chaque interface dans la couche qui la possède. OrderRepository se trouve à côté de PlaceOrder, pas à côté de son implémentation.
  • Utilisez deux styles de flèche : pleine pour les appels, en pointillés avec un triangle creux pour implémente, comme dans la réalisation UML. L'exemple de diagramme de classes UML couvre la notation.
  • Donnez une seule direction à l'intérieur sur la page. Une flèche qui revient est une violation ou un appel à l'exécution à étiqueter.
  • Étiquetez les flèches avec la méthode et le modèle qui franchit la frontière.
  • Omettez les champs des DTO, les éléments internes des frameworks et le conteneur d'injection de dépendances.

Pour le système autour de cette vue au niveau du code, consultez l'exemple de diagramme du modèle C4 et le guide pour dessiner des diagrammes d'architecture logicielle.

Le construire dans Archboard

  1. Connectez-vous avec Google ou GitHub et créez un diagramme.
  2. Ajoutez quatre nœuds Paquetage du pack UML côte à côte : Frameworks and drivers, Interface adapters, Use cases et Entities, avec des accents bleu, vert, rouge et ambre.
  3. Dans Frameworks and drivers, ajoutez un Composant du pack UML nommé Web framework et un nœud Postgres du pack Données nommé PostgreSQL.
  4. Dans Interface adapters, ajoutez deux nœuds Composant, OrderPresenter et OrderController, et une Classe nommée PostgresOrderRepository.
  5. Dans Use cases, ajoutez une Classe nommée PlaceOrder entre deux nœuds Interface, PlaceOrderOutput et OrderRepository.
  6. Dans Entities, ajoutez une Classe nommée Order avec ses champs et ses méthodes.
  7. Tracez six arêtes pleines étiquetées pour les appels, deux arêtes en pointillés avec des pointes en triangle pour implémente, et une arête en points de PostgresOrderRepository vers PostgreSQL.
  8. Ajoutez un bloc Markdown du pack Base avec la règle et les styles de ligne, et des notes Texte du pack Base pour chaque couche.

Un organigramme Mermaid de vos couches devient un premier brouillon avec l'outil de conversion de Mermaid en diagramme modifiable. L'offre Free autorise 15 nœuds de diagramme par diagramme ; suivez donc un use case par diagramme.

L'exemple utilise 14 nœuds de diagramme et reste dans les limites de l'offre Free.

FAQ

Quelles sont les quatre couches de la clean architecture ? Entities (règles métier de l'entreprise), use cases (règles métier de l'application), interface adapters (contrôleurs, presenters et gateways) et frameworks and drivers (le framework web, la base de données et d'autres outils). Martin dit que les cercles sont schématiques : il peut en falloir plus de quatre.

Qu'est-ce que la règle de dépendance ? Les dépendances du code source pointent uniquement vers l'intérieur. Le code d'une couche interne ne nomme jamais rien de déclaré dans une couche externe, donc les use cases ne savent rien des contrôleurs, du SQL ni du framework web.

Quelle est la différence entre clean architecture et architecture hexagonale ? Toutes deux gardent les règles métier au centre et font dépendre les adapters d'elles. L'architecture hexagonale sépare seulement l'intérieur de l'extérieur, au moyen de ports. La clean architecture divise aussi l'intérieur en entities et use cases, et nomme les presenters ainsi que les modèles de requête et de réponse.

Où place-t-on l'interface du repository en clean architecture ? Dans la couche des use cases, à côté du use case qui en a besoin. L'implémentation, avec le SQL, appartient à la couche des interface adapters, et le composition root la transmet au démarrage.

La clean architecture est-elle excessive pour les petits projets ? Souvent. Pour les services CRUD, les prototypes et les outils éphémères, les interfaces et les conversions supplémentaires coûtent plus qu'elles ne rapportent. Ajoutez des interfaces pour les dépendances que vous pensez voir changer, et faites grandir les couches quand de vraies règles métier apparaissent.

Commencez gratuitement — un siège, sans carte bancaire.

Articles associés

← Retour au blog

Ouvrez un canevas en une minute

Connectez-vous avec Google ou GitHub. Gratuit pour un siège. Passez à un forfait supérieur quand l’équipe vous rejoint.