Pourquoi choisir la composition plutôt que l’héritage
Julien on mai 30th 2008
Un ami m'a récemment demandé si je pensais qu'il était acceptable d'avoir un héritage ayant une profondeur de 10 classes. Je ne pense pas que cela le soit. Si vous prenez une classe en bas de la hiérarchie, une modification sur n'importe laquelle des 9 classes supérieures peut introduire une régression. Cela peut rapidement devenir un sacré cauchemar à maintenir!
La plupart du temps, si vous avez une hiérarchie de ce type dans votre application, c'est que vous avez choisi de privilégier l'héritage sur la composition. C'est probablement aussi un "code-smell" indiquant que votre classe fait trop de chose. Afin d'éviter cela, laissez moi vous montrer comment il est possible d'étendre une classe sans utiliser l'héritage.
Partons du principe que nous commençons avec le code suivant. Je sais que c'est un example très basique, mais cela sera suffisant pour notre démonstration.
abstract class TripBase { private readonly string _from; private readonly string _to; public string From { get { return _from; } } public string To { get { return _to; } } protected TripBase(string from, string to) { _from = from; _to = to; } public abstract DateTime CalculateEstimatedArrivalTime(); } class CarTrip : TripBase { public CarTrip(string from, string to) : base(from, to) { } public override DateTime CalculateEstimatedArrivalTime() { return DateTime.Now.AddHours(15); } } class PlaneTrip : TripBase { public PlaneTrip(string from, string to) : base(from, to) { } public override DateTime CalculateEstimatedArrivalTime() { return DateTime.Now.AddHours(2); } }
Nous avons une classe abstraite, BaseTrip, et nous voulons l'étendre en ayant 2 sous-classes: CarTrip et PlaneTrip. Dans une application réelle, nous aurions un algorithme pour calculer ou récupérer le temps de transport. Il serait d'ailleurs probablement très différent pour chaque classe. Dans notre exemple, afin de simplifier, nous retournons juste une valeur codée en dur.
Ce code n'est pas mauvais, loin de là. Cependant il est également loin d'être parfait. Par exemple, sommes-nous sur que le calcul du temps de transport entre 2 villes est la responsabilité de nos classes CarTrip et PlaneTrip? En effet, si nous voulons ajouter une nouvelle classe MotocycleTrip qui aurait des propriétés différentes mais le même algorithme de calcul de temps de transport que CarTrip, nous sommes condamné à extraire une nouvelle super-classe abstraite que nous appelerions par exemple RoadTrip... Cela semble beaucoup de travail pour pas grand chose...
Voici une version refactorisée de ces classes, utilisant la composition plutôt que l'héritage:
class Trip { private readonly string _from; private readonly string _to; private readonly ITransportationMode _transportationMode; public string From { get { return _from; } } public string To { get { return _to; } } protected Trip(string from, string to, ITransportationMode transportationMode) { _from = from; _to = to; _transportationMode = transportationMode; } public DateTime CalculateEstimatedArrivalTime() { return DateTime.Now.Add(_transportationMode.CalculateTransportationTimeBetween(_from, _to)); } } internal interface ITransportationMode { TimeSpan CalculateTransportationTimeBetween(string from, string to); } class CarTransportationMode : ITransportationMode { public TimeSpan CalculateTransportationTimeBetween(string from, string to) { } } class PlaneTransportationMode : ITransportationMode { public TimeSpan CalculateTransportationTimeBetween(string from, string to) { } }
En quelques mots, j'ai extrait les calculs dans leurs propres classes qui implémentent l'inferface ITransportationMode. Les instances sont ensuite injectées par le constructeur (mais nous pourrions également les injecter par un setter ou une méthode).
Cette implémentation améliore le code de plusieurs façons:
- Le calcul du temps de transport est réalisé dans une nouvelle classe, cela ne pollue donc plus la classe Trip. Il est beaucoup plus facile d'ajouter de nouveaux algorithmes de calculs, il suffit d'ajouter de nouvelles implémentation de ITransportationMode. Il est donc possible de les faire évoluer indépendamment de la classe Trip, ce qu'on appelle en anglais la "Separation of Concerns" (honnêtement, je ne sais pas comment traduire ce terme!).
- Nous pouvons maintenant configurer notre classe Trip. Avant cela, il était nécessaire de créer une nouvelle instance d'une sous-classe de TribBase pour modifier la façon dans le calcul était fait. Maintenant il nous suffit juste de changer l'instance de ITransportationMode tout en gardant le même objet Trip.
- Nous pouvons avoir plusieurs sous-classes de Trip qui utiliseraient le même algorithme sans introduire une nouvelle classe abstraite (comme RoadTrip par exemple). En conséquent, la composition aide à garder nos hiérarchies de classes aussi plates que possible.
Filed in Alt.net Foundations | No responses yet

