Introduction : quand SOLID cesse d'être une théorie
\nLa plupart des développeurs connaissent l'abréviation SOLID au niveau "une classe doit être responsable d'une seule chose". Cependant, dans les projets réels en C# (.NET) avec Dependency Injection, microservices et patrons asynchrones, une application naïve des principes produit l'effet inverse — complexité excessive, gonflement des interfaces et rupture de l'encapsulation.
\nDans cet article, nous considérons SOLID non pas comme un ensemble de dogmes, mais comme un outil pour prendre des décisions architecturales. Vous apprendrez à appliquer correctement les principes dans des scénarios avancés : travail avec les conteneurs IoC, composition plutôt qu'héritage, gestion des erreurs via les interfaces et test des sagas.
\n\n1. Single Responsibility : limites de responsabilité en DDD
\nLe principe de responsabilité unique (SRP) est souvent confondu avec "une classe — une fonction". En réalité, il s'agit de la raison du changement (reason to change). Dans le contexte de C# et du Domain-Driven Design, cela signifie qu'une classe ne doit être modifiée que pour une seule raison, liée à un seul acteur ou à une règle métier.
\n\nAnti-patron avancé : "l'Entité Divine"
\nConsidérons un exemple typique de violation du SRP dans .NET — la classe OrderEntity, qui gère simultanément l'état de la commande, calcule les taxes, envoie un email et sauvegarde les données dans la base de données via Entity Framework.
// ❌ Violation du SRP\npublic class OrderEntity\n{\n public int Id { get; set; }\n public decimal Total { get; set; }\n public string Email { get; set; }\n\n public void CalculateTax() { /* logique de taxes */ }\n public void SendConfirmation() { /* envoi d'email */ }\n public void SaveToDatabase() { /* EF Core SaveChanges */ }\n}\n\nProblème : une modification de la règle métier concernant les taxes (par exemple, une nouvelle loi) entraînera une modification de la classe qui est également responsable de l'envoi des notifications. Cela augmente le risque de régression.
\n\nSolution : séparation des services via DI
\n// ✅ SRP via les services\npublic class Order\n{\n public int Id { get; set; }\n public decimal Total { get; set; }\n // Uniquement l'état, aucune logique\n}\n\npublic class TaxCalculator : ITaxCalculator\n{\n public decimal Calculate(Order order) { /* ... */ }\n}\n\npublic class EmailService : INotificationService\n{\n public Task SendConfirmation(Order order) { /* ... */ }\n}\n\npublic class OrderRepository : IRepository<Order>\n{\n public Task SaveAsync(Order order) { /* EF Core */ }\n}\n\nDésormais, chaque classe a exactement une raison d'être modifiée. Le conteneur DI (Microsoft.Extensions.DependencyInjection) les lie via les constructeurs.
\n\n2. Open/Closed : extension via stratégie et décorateur
\nLe principe d'ouverture/fermeture (OCP) stipule : une classe doit être ouverte à l'extension, mais fermée à la modification. Dans .NET, cela est réalisé non seulement par l'héritage, mais aussi par la composition avec les patrons Strategy et Decorator.
\n\nCas avancé : validation avec augmentation des règles
\nImaginez que vous ayez un système de validation des commandes. La version initiale ne vérifiait que le montant et l'adresse. Un mois plus tard, une règle "ne pas commander des articles pour un montant supérieur à la limite" a été ajoutée. Au lieu de modifier la classe OrderValidator, utilisons la composition.
// ❌ Violation de l'OCP — modification d'une classe existante\npublic class OrderValidator\n{\n public bool Validate(Order order)\n {\n if (order.Total < 0) return false;\n if (string.IsNullOrEmpty(order.Address)) return false;\n // Nouvelle règle — nous modifions la classe\n if (order.Total > 10000) return false;\n return true;\n }\n}\n\n\n// ✅ OCP via composition de validateurs\npublic interface IValidator<T>\n{\n bool Validate(T entity);\n}\n\npublic class TotalValidator : IValidator<Order>\n{\n public bool Validate(Order order) => order.Total > 0;\n}\n\npu