Aller au contenu

Clean Architecture

Introduction

Une application peut être simple à créer, mais difficile à faire évoluer à un stade de son développement. Au fur et à mesure que nous ajoutons des fonctionnalités, il arrive que des développeurs implémentent des règles métier dans les actions des contrôleurs ASP.NET Core, ainsi que des requêtes Linq To Entities, etc. Une modification technique impose alors de modifier des blocs de code contenant des règles métier, pour lesquels les spécifications n'ont pas changées.

La Clean Architecture propose d’organiser le code autour des éléments donnant de la valeur à l’application : ses règles métier et ses cas d’utilisation. Elle définit des frontières entre le cœur du logiciel et ses aspects techniques, avec une règle essentielle : les dépendances du code pointent vers le cœur.

Qu’est-ce que la Clean Architecture ?

La Clean Architecture est une manière de répartir les responsabilités et de contrôler les dépendances du code. Dans une application .NET, on retrouve souvent quatre ensembles :

Clean Architecture - Exemple

Ce découpage correspond à une organisation du code, pas un nombre imposé de projets, de serveurs ou de processus. Le domaine n’a pas besoin de connaître ASP.NET Core et EF Core. L’application peut dépendre du domaine ; l’infrastructure et la présentation dépendent des contrats nécessaires au cœur. Selon le besoin, une interface peut être définie dans le domaine ou dans l’application, en fonction du code où elle doit être utilisée.

La règle des dépendances

Le sens des appels à l’exécution et le sens des références entre projets ne sont pas identiques. Un cas d’utilisation peut appeler IOrderRepository, tandis qu’une classe EF Core de l’infrastructure implémente cette interface. Le cas d’utilisation ne référence ainsi aucune classe EF Core.

Clean Architecture - Dépendances du code

Ce dessin illustre une organisation possible : l’API référence l’application et l’infrastructure pour enregistrer les implémentations au démarrage. Les dépendances exactes varient selon les projets, mais le domaine reste indépendant des détails externes.

Exemple en C# : valider une commande

Dans un logiciel, nous devons implémenter l'ajout d'une commande :

public interface IOrderRepository
{
    Task<Order?> GetAsync(Guid id, CancellationToken cancellationToken);
    Task SaveAsync(Order order, CancellationToken cancellationToken);
}

public sealed class ValidateOrder
{
    private readonly IOrderRepository _orders;

    public ValidateOrder(IOrderRepository orders)
    {
        _orders = orders;
    }

    public async Task ExecuteAsync(Guid id, CancellationToken cancellationToken)
    {
        var order = await _orders.GetAsync(id, cancellationToken)
            ?? throw new InvalidOperationException("Order not found.");

        order.Validate(); // The business rule belongs to Order.
        await _orders.SaveAsync(order, cancellationToken);
    }
}

L’infrastructure contient une implémentation de l’interfaceIOrderRepository utilisant EF Core. L’API enregistre dans le conteneur d'injections de dépendance les services permettant d'ajouter une commande :

builder.Services.AddScoped<IOrderRepository, EfCoreOrderRepository>();
builder.Services.AddScoped<ValidateOrder>();

Les avantages

Des responsabilités plus lisibles

Les traitements qui modifient l’état et ceux qui consultent les données sont identifiables séparément.

Des lectures adaptées aux usages

Chaque requête peut retourner les données nécessaires à un écran sans exposer tout le modèle d’écriture.

Des règles métier mieux protégées

Les commandes offrent un emplacement clair pour contrôler les modifications et préserver les invariants.

Des optimisations ciblées

Lorsque les besoins diffèrent fortement, lectures et écritures peuvent évoluer et être optimisées séparément.

Les limites

Un coût de structure

Les frontières, contrats et conversions ajoutent du code et peuvent peser sur une application très simple.

Des abstractions à concevoir

Des interfaces trop génériques ou calquées sur EF Core masquent peu de choses et compliquent les cas d’utilisation.

Une discipline nécessaire

Sans règles de références et de responsabilités, les détails techniques finissent par traverser les frontières.

Une indépendance relative

Changer de stockage ou de fournisseur peut modifier les transactions, les performances et certains besoins fonctionnels.

Quand choisir cette architecture ?

Elle devient pertinente lorsque les règles métier sont nombreuses ou appelées à évoluer, lorsque plusieurs endpoints utilisent les mêmes cas d’utilisation.

Pour une petite application CRUD ayant une durée de vie courte, le découpage peut être allégé. On peut commencer par de simples responsabilités et introduire des projets ou des contrats supplémentaires lorsque les dépendances deviennent plus difficiles à gérer. La Clean Architecture ne rend pas nécessaires le CQRS, le DDD, un médiateur ou des microservices : ces approches répondent à d’autres besoins et peuvent être combinées si elles apportent une valeur réelle.

Bonnes pratiques avec .NET

Faire respecter le sens des références

Garder le domaine indépendant et placer les dépendances techniques dans les projets externes.

Placer les contrats près de leurs utilisateurs

Définir chaque interface dans la couche qui en a besoin, sans créer une interface pour chaque classe.

Garder EF Core dans l’infrastructure

Y placer le DbContext, les configurations et les repositories, puis tester les accès aux données en intégration.

Modéliser les décisions métier dans le domaine

Confier les invariants aux entités et aux services de domaine plutôt qu’aux contrôleurs ou aux repositories.

Coordonner dans l’application

Utiliser les cas d’utilisation pour enchaîner les opérations et exposer les résultats attendus.

Assembler au démarrage

Enregistrer les implémentations avec l’injection de dépendances ASP.NET Core, à la frontière de l’application.

Adapter le découpage à la complexité

Ajouter des projets, contrats et conversions lorsqu’ils clarifient une frontière concrète.