Es gibt zwei Möglichkeiten, wie abstrakte Basisklassen verwendet werden.
-
Sie spezialisieren Ihr abstraktes Objekt, aber alle Clients werden die abgeleitete Klasse über ihre Basisschnittstelle verwenden.
-
Sie verwenden eine abstrakte Basisklasse, um die Duplizierung innerhalb von Objekten in Ihrem Entwurf zu vermeiden, und die Clients verwenden die konkreten Implementierungen über ihre eigenen Schnittstellen!
Lösung für 1 - Strategie-Muster
![Option1]()
Im ersten Fall haben Sie eine Schnittstelle, die durch die virtuellen Methoden in der abstrakten Klasse definiert ist, die von den abgeleiteten Klassen implementiert werden.
Sie sollten in Erwägung ziehen, diese Schnittstelle zu einer echten Schnittstelle zu machen, Ihre abstrakte Klasse in eine konkrete Klasse umzuwandeln und eine Instanz dieser Schnittstelle in ihren Konstruktor aufzunehmen. Ihre abgeleiteten Klassen werden dann zu Implementierungen dieser neuen Schnittstelle.
![IMotor]()
Das bedeutet, dass Sie jetzt Ihre zuvor abstrakte Klasse mit einer Mock-Instanz der neuen Schnittstelle und jede neue Implementierung über die jetzt öffentliche Schnittstelle testen können. Alles ist einfach und testbar.
Lösung für 2
In der zweiten Situation arbeitet Ihre abstrakte Klasse als Hilfsklasse.
![AbstractHelper]()
Werfen Sie einen Blick auf die darin enthaltenen Funktionen. Schauen Sie, ob etwas davon auf die zu bearbeitenden Objekte übertragen werden kann, um die Duplizierung zu minimieren. Wenn Sie noch etwas übrig haben, machen Sie es zu einer Hilfsklasse, die Ihre konkrete Implementierung in ihrem Konstruktor übernimmt und ihre Basisklasse entfernt.
![Motor Helper]()
Dies wiederum führt zu konkreten Klassen, die einfach und leicht testbar sind.
In der Regel
Bevorzugen Sie ein komplexes Netz von einfachen Objekten gegenüber einem einfachen Netz von komplexen Objekten.
Der Schlüssel zu erweiterbarem, testbarem Code sind kleine Bausteine und eine unabhängige Verdrahtung.
Aktualisiert: Wie kann man mit Mischungen aus beiden umgehen?
Es ist möglich, eine Basisklasse zu haben, die beide Aufgaben erfüllt, d. h. sie hat eine öffentliche Schnittstelle und geschützte Hilfsmethoden. Wenn dies der Fall ist, können Sie die Hilfsmethoden in eine Klasse auslagern (Szenario 2) und den Vererbungsbaum in ein Strategiemuster umwandeln.
Wenn Sie feststellen, dass einige Methoden von Ihrer Basisklasse direkt implementiert werden und andere virtuell sind, können Sie den Vererbungsbaum immer noch in ein Strategiemuster umwandeln, aber ich würde dies auch als einen guten Indikator dafür ansehen, dass die Verantwortlichkeiten nicht korrekt ausgerichtet sind und möglicherweise überarbeitet werden müssen.
Update 2: Abstrakte Klassen als Sprungbrett (2014/06/12)
Neulich hatte ich eine Situation, in der ich abstrakte Begriffe verwendet habe, und ich würde gerne herausfinden, warum.
Wir haben ein Standardformat für unsere Konfigurationsdateien. Dieses spezielle Tool hat 3 Konfigurationsdateien, die alle in diesem Format vorliegen. Ich wollte eine stark typisierte Klasse für jede Einstellungsdatei, so dass eine Klasse durch Dependency Injection nach den Einstellungen fragen kann, die für sie von Bedeutung sind.
Ich implementierte dies, indem ich eine abstrakte Basisklasse hatte, die weiß, wie man die Formate der Einstellungsdateien analysiert, und abgeleitete Klassen, die dieselben Methoden zur Verfügung stellten, aber den Speicherort der Einstellungsdatei kapselten.
Ich hätte einen "SettingsFileParser" schreiben können, den die 3 Klassen umhüllen, und dann an die Basisklasse delegieren können, um die Datenzugriffsmethoden freizulegen. Ich habe mich entschieden, dies nicht zu tun dennoch da dies zu 3 abgeleiteten Klassen mit mehr Delegation Code als alles andere.
Wie auch immer... wenn sich dieser Code weiterentwickelt und die Verbraucher jeder dieser Einstellungsklassen klarer werden. Jeder Einstellungsbenutzer wird nach einigen Einstellungen fragen und sie auf irgendeine Weise umwandeln (da Einstellungen Text sind, können sie in Objekte verpackt oder in Zahlen umgewandelt werden usw.). Wenn dies geschieht, werde ich damit beginnen, diese Logik in Datenmanipulationsmethoden zu extrahieren und sie in die stark typisierten Einstellungsklassen zurückzuschieben. Dies wird zu einer Schnittstelle auf höherer Ebene für jeden Satz von Einstellungen führen, die schließlich nicht mehr weiß, dass sie es mit "Einstellungen" zu tun hat.
An diesem Punkt benötigen die stark typisierten Einstellungsklassen keine "Getter"-Methoden mehr, die die zugrunde liegende "Settings"-Implementierung offenlegen.
An diesem Punkt würde ich nicht mehr wollen, dass ihre öffentliche Schnittstelle die Einstellungszugriffsmethoden enthält; also werde ich diese Klasse ändern, um eine Einstellungsparserklasse zu kapseln, anstatt von ihr abzuleiten.
Die abstrakte Klasse ist daher: eine Möglichkeit für mich, Delegationscode im Moment zu vermeiden, und eine Markierung im Code, die mich daran erinnert, das Design später zu ändern. Es kann sein, dass ich nie dazu komme, also kann sie eine ganze Weile leben... das kann nur der Code zeigen.
Ich finde, das trifft auf jede Regel zu... 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 übersehen haben... und lässt Sie in der Zwischenzeit Ihren Kunden weiterhin einen Mehrwert bieten.
Ich stelle mir vor, dass Regeln wie diese eine Landschaft definieren, in der wartbarer Code in den Tälern lebt. Wenn man neues Verhalten hinzufügt, ist das wie Regen, der auf den Code fällt. Am Anfang platziert man es dort, wo es landet, dann refaktorisiert man, damit die Kräfte des guten Designs das Verhalten umherschieben können, bis alles in den Tälern landet.