MVAESTECH
Guide .NET · 03 · Fondations

Publié le · par Mathieu Vaes

Design patterns

Les patterns sont un vocabulaire commun pour des solutions éprouvées. Un senior les reconnaît dans le framework, sait quand les appliquer, et surtout quand s'en passer.

Chapitres du guide
En clair

Un design pattern est une solution connue à un problème qui revient souvent, avec un nom partagé. Dire « on ajoute un décorateur de cache » suffit à toute l'équipe pour comprendre la structure, comme un architecte qui dit « mur porteur » sans dessiner le plan.

Les patterns qui servent vraiment en .NET Confirmé

FamillePatternProblème résoluDéjà présent dans .NET
CréationFactoryChoisir quelle classe instancier selon un contexteIHttpClientFactory, ILoggerFactory
BuilderConstruire un objet complexe étape par étapeWebApplication.CreateBuilder, StringBuilder
SingletonUne seule instance partagéeDurée de vie Singleton du conteneur DI (pas de champ static)
StructureAdapterBrancher une API externe sur votre interfaceAdaptateurs d'infrastructure (hexagonale)
DecoratorAjouter un comportement (cache, log, retry) sans modifier la classeDelegatingHandler de HttpClient, Stream (GZipStream enveloppe un flux)
FacadeSimplifier un sous-système compliquéUn service applicatif qui orchestre plusieurs dépendances
ProxyContrôler l'accès à un objetProxies de chargement différé d'EF Core, clients gRPC générés
ComportementStrategyAlgorithme interchangeable à l'exécutionIComparer<T>, services à clé (.NET 8+)
ObserverPrévenir des abonnés qu'un événement s'est produitevent, IObservable<T>, événements de domaine
Chain of ResponsibilityFaire passer une demande par une chaîne de traitementsPipeline de middleware, behaviors de médiateur
CommandReprésenter une action comme un objetCommandes CQRS, ICommand en MVVM
Template MethodSquelette d'algorithme dont certaines étapes varientBackgroundService.ExecuteAsync

La moitié du travail consiste à reconnaître ces patterns dans le framework : quand on sait que le pipeline de middleware est une chaîne de responsabilité, on comprend tout de suite pourquoi l'ordre compte.

Decorator, le plus rentable Confirmé

Un décorateur implémente la même interface que l'objet qu'il enveloppe, fait son travail, puis délègue. On empile les décorateurs comme des poupées russes : le code métier ne change jamais.

public interface IProductRepository
{
    Task<Product?> GetAsync(int id, CancellationToken ct);
}

// L'implémentation réelle, sans aucune préoccupation de cache
public sealed class SqlProductRepository(ShopDbContext db) : IProductRepository
{
    public Task<Product?> GetAsync(int id, CancellationToken ct) =>
        db.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id, ct);
}

// Le décorateur : même interface, enveloppe la précédente
public sealed class CachedProductRepository(IProductRepository inner, HybridCache cache) : IProductRepository
{
    public async Task<Product?> GetAsync(int id, CancellationToken ct) =>
        await cache.GetOrCreateAsync($"product:{id}",
            async token => await inner.GetAsync(id, token), cancellationToken: ct);
}

// Enregistrement avec Scrutor (package NuGet) : l'ordre d'appel suit l'ordre de déclaration
services.AddScoped<IProductRepository, SqlProductRepository>();
services.Decorate<IProductRepository, CachedProductRepository>();
services.Decorate<IProductRepository, LoggingProductRepository>();   // le plus externe

Strategy avec les services à clé Confirmé

public interface IShippingCalculator { Money Compute(Order order); }

services.AddKeyedSingleton<IShippingCalculator, ColissimoCalculator>("colissimo");
services.AddKeyedSingleton<IShippingCalculator, ChronopostCalculator>("chronopost");

// Choix de la stratégie à l'exécution, sans if/switch dans le code métier
public sealed class ShippingService(IServiceProvider sp)
{
    public Money Quote(Order order, string carrier) =>
        sp.GetRequiredKeyedService<IShippingCalculator>(carrier).Compute(order);
}

// Ou injection directe d'une stratégie précise
public sealed class ExpressHandler([FromKeyedServices("chronopost")] IShippingCalculator calc) { }

Specification et Result Senior

Specification

Encapsule une règle de sélection réutilisable (« commande en retard ») sous forme d'Expression<Func<T, bool>>, traduisible en SQL par EF Core et testable seule.

Result

Remplace les exceptions pour les échecs attendus : Result<Order> porte soit la valeur, soit une erreur typée. L'appelant est obligé de traiter les deux cas.

public static class OrderSpecs
{
    public static Expression<Func<Order, bool>> Late(DateTimeOffset now) =>
        o => o.Status == OrderStatus.Submitted && o.PromisedAt < now;
}
var late = await db.Orders.Where(OrderSpecs.Late(clock.GetUtcNow())).ToListAsync(ct);
Anti-patterns
  • Service Locator : injecter IServiceProvider partout et résoudre à la main cache les dépendances réelles. Acceptable seulement dans une factory comme ci-dessus.
  • Singleton statique (public static Instance) : impossible à remplacer en test, état global partagé.
  • God class : une classe « Manager » ou « Helper » qui fait tout.
  • Patternite : une factory d'une factory pour une seule implémentation. Un pattern se justifie par un problème présent, pas par une possibilité future.

À retenir

  • Reconnaître les patterns du framework : middleware = chaîne, DelegatingHandler = décorateur.
  • Decorator pour le transverse, Strategy pour l'interchangeable, Adapter pour l'externe.
  • Le code le plus simple qui résout le problème réel gagne contre le pattern élégant.

Un projet .NET à cadrer ou à reprendre ?

API REST documentées et testées, applications ASP.NET MVC (y compris VB.NET), migration de services anciens vers une architecture claire : parlons de votre code.