Introduction : le piège dans lequel beaucoup sont tombés
\nImaginez la situation : vous écrivez une application pour une bibliothèque en ligne. Vous avez besoin de votre propre classe Library, qui stocke des livres, sait les trier, les rechercher et afficher des statistiques. La première chose qui vient à l'esprit est d'hériter de List<Book>. Après tout, il a déjà tout : Add, Remove, l'indexation, la recherche. Logique ? Il semblerait que oui.
Mais les développeurs C# expérimentés vous diront : « N'héritez jamais de List<T> ». Cette question sur Stack Overflow a recueilli des milliers de vues, et ce n'est pas pour rien. Derrière la simplicité apparente se cachent de sérieux problèmes d'architecture, de performance et, surtout, de violation des principes de conception orientée objet.
\nDans cet article, nous allons regarder sous le capot de List<T>, comprendre pourquoi en hériter est un anti-patron de conception, et quoi utiliser à la place. Prêts ? C'est parti !
Qu'est-ce qui ne va pas avec List<T> du point de vue de la conception ?
\nCommençons par l'essentiel : List<T> n'a pas été conçu comme une base pour une hiérarchie, mais comme une implémentation concrète et hautes performances d'un tableau dynamique. Son API publique est un ensemble de méthodes pour travailler avec une collection, et non un contrat pour l'extension.
Regardez sa déclaration dans les sources de .NET (simplifiée) :
\npublic class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable\n{\n private T[] _items;\n private int _size;\n \n public void Add(T item) { /* ... */ }\n public bool Remove(T item) { /* ... */ }\n // ... des dizaines d'autres méthodes\n}\nNotez que List<T> n'est pas marqué comme abstract. Il ne fournit pas de méthodes virtuelles à redéfinir (à l'exception de quelques-unes, dont nous parlerons plus tard). Cela signifie que vous ne pouvez pas modifier son comportement en toute sécurité. Si vous redéfinissez Add, vous risquez de casser la logique interne qui dépend de cette méthode.
Problème n°1 : Violation du principe de substitution de Liskov (LSP)
\nLe principe de substitution de Liskov stipule : « Une classe héritière doit compléter, et non remplacer, le comportement de l'ancêtre ». Lorsque vous héritez de List<T>, vous promettez que votre classe peut être utilisée partout où un List<T> est attendu. Mais cela est rarement respecté.
Exemple : vous créez une classe ReadOnlyLibrary : List<Book> et redéfinissez Add pour qu'il lève une exception :
public class ReadOnlyLibrary : List<Book>\n{\n public new void Add(Book book)\n {\n throw new InvalidOperationException("Collection en lecture seule !");\n }\n}\n\n// Quelque part dans le code :\nList<Book> list = new ReadOnlyLibrary();\nlist.Add(new Book()); // Oups ! Aucune exception ne sera levée !\n// Parce que new ne redéfinit pas, mais masque la méthode.\n// C'est List.Add qui sera appelé, pas le nôtre.\nMême en utilisant override (et List.Add n'est pas virtuel), vous ne pouvez pas interdire l'ajout — l'interface IList ne le prévoit pas. Vous brisez simplement le contrat.
Problème n°2 : Violation de l'encapsulation
\nEn héritant de List<T>, vous exposez toute son API publique vers l'extérieur. Votre classe Library héritera des méthodes Sort, BinarySearch, Reverse, Insert, etc. Mais que faire si vous voulez que les livres soient toujours triés par auteur ? L'utilisateur de votre classe pourra appeler library.Reverse() et briser l'invariant.
Vous ne pourrez pas l'empêcher sans réécrire toutes les méthodes — et il y en a des dizaines. C'est une violation de l'encapsulation : vous exposez une implémentation interne que vous ne pouvez pas contrôler.
\n\nRegardons sous le capot : comment List<T> est structuré à l'intérieur
\nPour comp