Ich persönlich habe fast nie das Bedürfnis, abstrakte Klassen zu schreiben.
Wenn ich sehe, dass abstrakte Klassen (falsch) verwendet werden, liegt das meist daran, dass der Autor der abstrakten Klasse das Muster "Schablonenmethode" verwendet.
Das Problem mit der "Schablonenmethode" ist, dass sie fast immer etwas reentrant ist - die "abgeleitete" Klasse weiß nicht nur von der "abstrakten" Methode ihrer Basisklasse, die sie implementiert, sondern auch von den öffentlichen Methoden der Basisklasse, obwohl sie diese meistens nicht aufrufen muss.
(Übermäßig vereinfachtes) Beispiel:
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);
}
Hier hat der Autor dieser Klasse also einen allgemeinen Algorithmus geschrieben und möchte, dass andere ihn verwenden, indem sie ihn "spezialisieren", indem sie ihre eigenen "Haken" bereitstellen - in diesem Fall eine "Vergleichsmethode".
Der Verwendungszweck ist also etwa so:
class NameSorter : QuickSorter
{
public bool compare(object lhs, object rhs)
{
// etc.
}
}
Das Problem dabei ist, dass Sie zwei Konzepte in unzulässiger Weise miteinander verknüpft haben:
- Eine Möglichkeit, zwei Gegenstände zu vergleichen (welcher Gegenstand sollte zuerst gehen)
- Eine Methode zum Sortieren von Elementen (z. B. Schnellsortierung vs. Mischsortierung usw.)
Im obigen Code kann der Autor der "compare"-Methode theoretisch reentrant zurück in die "Sort"-Methode der Superklasse aufrufen... auch wenn sie dies in der Praxis nie tun wollen oder müssen.
Der Preis, den Sie für diese unnötige Kopplung zahlen, ist, dass es schwierig ist, die Oberklasse zu ändern, und in den meisten OO-Sprachen ist es unmöglich, sie zur Laufzeit zu ändern.
Die alternative Methode besteht darin, stattdessen das Entwurfsmuster "Strategie" zu verwenden:
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;
}
}
Also merken Sie sich das jetzt: Alles, was wir haben, sind Schnittstellen und konkrete Implementierungen dieser Schnittstellen. In der Praxis braucht man eigentlich nichts anderes, um ein OO-Design auf hohem Niveau zu erstellen.
Um die Tatsache zu "verstecken", dass wir die "Sortierung von Namen" durch die Verwendung einer "QuickSort"-Klasse und eines "NameComparator" implementiert haben, könnten wir noch irgendwo eine Fabrikmethode schreiben:
ISorter CreateNameSorter()
{
return new QuickSorter(new NameComparator());
}
Jede wenn Sie eine abstrakte Klasse haben, können Sie dies tun... selbst wenn es eine natürliche Wiedereingliederungsbeziehung zwischen der Basis- und der abgeleiteten Klasse gibt, lohnt es sich in der Regel, diese explizit zu machen.
Ein letzter Gedanke: Alles, was wir oben getan haben, ist, eine "NameSorting"-Funktion aus einer "QuickSort"-Funktion und einer "NameComparison"-Funktion zu "komponieren"... in einer funktionalen Programmiersprache wird diese Art der Programmierung noch natürlicher, mit weniger Code.