Es gibt zwei Möglichkeiten, wie abstrakte Basisklassen verwendet werden.
Sie spezialisieren Ihr abstraktes Objekt, aber alle Clients verwenden die abgeleitete Klasse über ihre Basisschnittstelle.
Sie verwenden eine abstrakte Basisklasse, um die Duplizierung innerhalb von Objekten in Ihrem Design herauszufiltern, und Clients verwenden die konkreten Implementierungen über ihre eigenen Schnittstellen.!
Lösung für 1 - Strategiemuster
Wenn Sie die erste Situation haben, haben Sie tatsächlich eine Schnittstelle, die durch die virtuellen Methoden in der abstrakten Klasse definiert ist, die Ihre abgeleiteten Klassen implementieren.
Sie sollten in Betracht ziehen, dies zu einer echten Schnittstelle zu machen, Ihre abstrakte Klasse konkret zu ändern und eine Instanz dieser Schnittstelle in ihren Konstruktor aufzunehmen. Ihre abgeleiteten Klassen werden dann zu Implementierungen dieser neuen Schnittstelle.
Dies bedeutet, dass Sie Ihre zuvor abstrakte Klasse jetzt mithilfe einer Scheininstanz der neuen Schnittstelle und jeder neuen Implementierung über die jetzt öffentliche Schnittstelle testen können. Alles ist einfach und überprüfbar.
Lösung für 2
Wenn Sie die zweite Situation haben, arbeitet Ihre abstrakte Klasse als Hilfsklasse.
Schauen Sie sich die darin enthaltenen Funktionen an. Überprüfen Sie, ob etwas davon auf die Objekte verschoben werden kann, die bearbeitet werden, um diese Duplizierung zu minimieren. Wenn Sie noch etwas übrig haben, versuchen Sie, es zu einer Hilfsklasse zu machen, die Ihre konkrete Implementierung in ihrem Konstruktor übernimmt, und entfernen Sie ihre Basisklasse.
Dies führt wiederum zu konkreten Klassen, die einfach und leicht zu testen sind.
Als Regel
Bevorzugen Sie ein komplexes Netzwerk einfacher Objekte gegenüber einem einfachen Netzwerk komplexer Objekte.
Der Schlüssel zu erweiterbarem testbarem Code sind kleine Bausteine und unabhängige Verkabelung.
Aktualisiert: Wie gehe ich mit Gemischen von beiden um?
Es ist möglich, dass eine Basisklasse beide Rollen ausführt ... dh: Sie verfügt über eine öffentliche Schnittstelle und geschützte Hilfsmethoden. Wenn dies der Fall ist, können Sie die Hilfsmethoden in eine Klasse zerlegen (Szenario 2) und den Vererbungsbaum in ein Strategiemuster konvertieren.
Wenn Sie feststellen, dass Sie einige Methoden haben, die Ihre Basisklasse direkt implementiert, und andere virtuell sind, können Sie den Vererbungsbaum dennoch in ein Strategiemuster konvertieren. Ich würde dies jedoch auch als guten Indikator dafür ansehen, dass die Verantwortlichkeiten nicht korrekt ausgerichtet sind und möglicherweise brauchen Refactoring.
Update 2: Abstract Classes als Sprungbrett (12.06.2014)
Ich hatte neulich eine Situation, in der ich abstrakt verwendet habe, deshalb möchte ich herausfinden, warum.
Wir haben ein Standardformat für unsere Konfigurationsdateien. Dieses spezielle Tool verfügt über 3 Konfigurationsdateien in diesem Format. Ich wollte eine stark typisierte Klasse für jede Einstellungsdatei, damit eine Klasse durch Abhängigkeitsinjektion nach den Einstellungen fragen kann, die sie interessiert.
Ich habe dies implementiert, indem ich eine abstrakte Basisklasse hatte, die weiß, wie die Einstellungsdateiformate und abgeleiteten Klassen analysiert werden, die dieselben Methoden verfügbar gemacht haben, aber den Speicherort der Einstellungsdatei gekapselt haben.
Ich hätte einen "SettingsFileParser" schreiben können, den die drei Klassen umschlossen und dann an die Basisklasse delegiert haben, um die Datenzugriffsmethoden verfügbar zu machen. Ich habe mich entschieden, dies noch nicht zu tun, da dies zu 3 abgeleiteten Klassen mit mehr Delegierungscode als alles andere führen würde.
Jedoch ... wenn sich dieser Code weiterentwickelt und die Verbraucher jeder dieser Einstellungsklassen klarer werden. Bei jedem Einstellungsbenutzer werden einige Einstellungen angefordert und auf irgendeine Weise transformiert (da es sich bei den Einstellungen um Text handelt, können sie in Objekte eingeschlossen werden, um sie in Zahlen usw. umzuwandeln). In diesem Fall werde ich beginnen, diese Logik in Datenmanipulationsmethoden zu extrahieren und sie wieder auf die stark typisierten Einstellungsklassen zu übertragen. Dies führt zu einer übergeordneten Benutzeroberfläche für jeden Satz von Einstellungen, die schließlich nicht mehr weiß, dass es sich um "Einstellungen" handelt.
Zu diesem Zeitpunkt benötigen die stark typisierten Einstellungsklassen nicht mehr die "Getter" -Methoden, die die zugrunde liegende Implementierung der Einstellungen verfügbar machen.
Zu diesem Zeitpunkt möchte ich nicht mehr, dass die öffentliche Schnittstelle die Einstellungen für die Zugriffsmethoden enthält. Daher werde ich diese Klasse ändern, um eine Parser-Klasse für Einstellungen zu kapseln, anstatt sie abzuleiten.
Die Abstract-Klasse ist daher: eine Möglichkeit für mich, im Moment Delegierungscode zu vermeiden, und eine Markierung im Code, die mich daran erinnert, das Design später zu ändern. Ich werde es vielleicht nie erreichen, also kann es eine Weile dauern ... nur der Code kann es sagen.
Ich finde, dass dies mit jeder Regel zutrifft ... wie "keine statischen Methoden" oder "keine privaten Methoden". Sie weisen auf einen Geruch im Code hin ... und das ist gut so. Es hält Sie auf der Suche nach der Abstraktion, die Sie verpasst haben ... und ermöglicht es Ihnen, Ihrem Kunden in der Zwischenzeit weiterhin einen Mehrwert zu bieten.
Ich stelle mir Regeln wie diese vor, die eine Landschaft definieren, in der wartbarer Code in den Tälern lebt. Wenn Sie neues Verhalten hinzufügen, ist es, als würde Regen auf Ihrem Code landen. Zuerst platzieren Sie es dort, wo es landet. Dann überarbeiten Sie es, damit die Kräfte guten Designs das Verhalten herumschieben können, bis alles in den Tälern landet.