Personnellement, je n'ai presque jamais besoin d'écrire des cours abstraits.
La plupart du temps, je vois des classes abstraites être (mal) utilisées, c'est parce que l'auteur de la classe abstraite utilise le modèle "Méthode de modèle".
Le problème avec la "méthode Template" est qu'elle est presque toujours rentrante - la classe "dérivée" connaît non seulement la méthode "abstraite" de sa classe de base qu'elle implémente, mais aussi les méthodes publiques de la classe de base , même si la plupart du temps il n'a pas besoin de les appeler.
Exemple (trop simplifié):
abstract class QuickSorter
{
public void Sort(object[] items)
{
// implementation code that somewhere along the way calls:
bool less = compare(x,y);
// ... more implementation code
}
abstract bool compare(object lhs, object rhs);
}
Donc, ici, l'auteur de cette classe a écrit un algorithme générique et a l'intention que les gens l'utilisent en le "spécialisant" en fournissant leurs propres "hooks" - dans ce cas, une méthode "compare".
Donc, l'utilisation prévue est quelque chose comme ceci:
class NameSorter : QuickSorter
{
public bool compare(object lhs, object rhs)
{
// etc.
}
}
Le problème est que vous avez indûment couplé deux concepts:
- Une façon de comparer deux articles (quel article doit aller en premier)
- Une méthode de tri des éléments (par exemple tri rapide vs tri par fusion, etc.)
Dans le code ci-dessus, théoriquement, l'auteur de la méthode "compare" peut rappeler de manière réentrante dans la méthode "Sort" de la superclasse ... même si en pratique ils ne voudront ou n'auront jamais besoin de le faire.
Le prix que vous payez pour ce couplage inutile est qu'il est difficile de changer la superclasse, et dans la plupart des langues OO, impossible de la changer au moment de l'exécution.
La méthode alternative consiste à utiliser le modèle de conception "Stratégie" à la place:
interface IComparator
{
bool compare(object lhs, object rhs);
}
class QuickSorter
{
private readonly IComparator comparator;
public QuickSorter(IComparator comparator)
{
this.comparator = comparator;
}
public void Sort(object[] items)
{
// usual code but call comparator.Compare();
}
}
class NameComparator : IComparator
{
bool compare(object lhs, object rhs)
{
// same code as before;
}
}
Alors remarquez maintenant: tout ce que nous avons, ce sont des interfaces et des implémentations concrètes de ces interfaces. En pratique, vous n'avez pas vraiment besoin d'autre chose pour faire une conception OO de haut niveau.
Pour "masquer" le fait que nous avons implémenté le "tri des noms" en utilisant une classe "QuickSort" et un "NameComparator", nous pourrions encore écrire une méthode d'usine quelque part:
ISorter CreateNameSorter()
{
return new QuickSorter(new NameComparator());
}
Chaque fois que vous avez une classe abstraite, vous pouvez le faire ... même lorsqu'il existe une relation de ré-entrée naturelle entre la classe de base et la classe dérivée, il est généralement avantageux de les rendre explicites.
Une dernière pensée: tout ce que nous avons fait ci-dessus est de "composer" une fonction "NameSorting" en utilisant une fonction "QuickSort" et une fonction "NameComparison" ... dans un langage de programmation fonctionnel, ce style de programmation devient encore plus naturel, avec moins de code.