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é
| Famille | Pattern | Problème résolu | Déjà présent dans .NET |
|---|---|---|---|
| Création | Factory | Choisir quelle classe instancier selon un contexte | IHttpClientFactory, ILoggerFactory |
| Builder | Construire un objet complexe étape par étape | WebApplication.CreateBuilder, StringBuilder | |
| Singleton | Une seule instance partagée | Durée de vie Singleton du conteneur DI (pas de champ static) | |
| Structure | Adapter | Brancher une API externe sur votre interface | Adaptateurs d'infrastructure (hexagonale) |
| Decorator | Ajouter un comportement (cache, log, retry) sans modifier la classe | DelegatingHandler de HttpClient, Stream (GZipStream enveloppe un flux) | |
| Facade | Simplifier un sous-système compliqué | Un service applicatif qui orchestre plusieurs dépendances | |
| Proxy | Contrôler l'accès à un objet | Proxies de chargement différé d'EF Core, clients gRPC générés | |
| Comportement | Strategy | Algorithme interchangeable à l'exécution | IComparer<T>, services à clé (.NET 8+) |
| Observer | Prévenir des abonnés qu'un événement s'est produit | event, IObservable<T>, événements de domaine | |
| Chain of Responsibility | Faire passer une demande par une chaîne de traitements | Pipeline de middleware, behaviors de médiateur | |
| Command | Représenter une action comme un objet | Commandes CQRS, ICommand en MVVM | |
| Template Method | Squelette d'algorithme dont certaines étapes varient | BackgroundService.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.
GetProduct(42) deux fois de suite.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);
- Service Locator : injecter
IServiceProviderpartout 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.