Ich bin in letzter Zeit besorgt über die Verwendung von abstrakten Klassen.
Manchmal wird im Voraus eine abstrakte Klasse erstellt, die als Vorlage für die Funktionsweise der abgeleiteten Klassen dient. Das bedeutet mehr oder weniger, dass sie einige Funktionen auf hoher Ebene bieten, aber bestimmte Details auslassen, die von abgeleiteten Klassen implementiert werden müssen. Die abstrakte Klasse definiert die Notwendigkeit dieser Details, indem sie einige abstrakte Methoden einführt. In solchen Fällen funktioniert eine abstrakte Klasse wie eine Blaupause, eine allgemeine Beschreibung der Funktionalität oder wie auch immer Sie sie nennen möchten. Es kann nicht alleine verwendet werden, sondern muss spezialisiert sein, um die Details zu definieren, die bei der Implementierung auf hoher Ebene nicht berücksichtigt wurden.
Manchmal kommt es vor, dass die abstrakte Klasse nach der Erstellung einiger "abgeleiteter" Klassen erstellt wird (da die übergeordnete / abstrakte Klasse nicht vorhanden ist, wurden sie noch nicht abgeleitet, aber Sie wissen, was ich meine). In diesen Fällen wird die abstrakte Klasse normalerweise als Ort verwendet, an dem Sie jede Art von allgemeinem Code einfügen können, den aktuell abgeleitete Klassen enthalten.
Nachdem ich die obigen Beobachtungen gemacht habe, frage ich mich, welcher dieser beiden Fälle die Regel sein sollte. Sollten irgendwelche Details in die abstrakte Klasse eingeblasen werden, nur weil sie derzeit in allen abgeleiteten Klassen gemeinsam sind? Sollte allgemeiner Code vorhanden sein, der nicht Teil einer High-Level-Funktionalität ist?
Sollte Code vorhanden sein, der für die abstrakte Klasse selbst möglicherweise keine Bedeutung hat, nur weil er zufällig für die abgeleiteten Klassen gleich ist?
Lassen Sie mich ein Beispiel geben: Die abstrakte Klasse A hat eine Methode a () und eine abstrakte Methode aq (). Die Methode aq () verwendet in beiden abgeleiteten Klassen AB und AC eine Methode b (). Sollte b () nach A verschoben werden? Wenn ja, dann würde die Existenz von b () nicht viel Sinn machen, wenn jemand nur A ansieht (tun wir so, als wären AB und AC nicht da)! Ist das eine schlechte Sache? Sollte jemand in der Lage sein, einen Blick in eine abstrakte Klasse zu werfen und zu verstehen, was vor sich geht, ohne die abgeleiteten Klassen zu besuchen?
Um ehrlich zu sein, glaube ich im Moment, dass das Schreiben einer abstrakten Klasse, die Sinn macht, ohne in die abgeleiteten Klassen schauen zu müssen, eine Frage von sauberem Code und sauberer Architektur ist. Ι Die Idee einer abstrakten Klasse, die sich wie ein Speicherauszug für jede Art von Code verhält, ist in allen abgeleiteten Klassen nicht üblich.
Was denkst / übst du?
quelle
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.
-- Warum? Ist es nicht möglich, dass ich beim Entwerfen und Entwickeln einer Anwendung feststelle, dass mehrere Klassen gemeinsame Funktionen haben, die natürlich in eine abstrakte Klasse umgewandelt werden können? Muss ich hellsichtig genug sein, um dies immer vorwegzunehmen, bevor ich Code für abgeleitete Klassen schreibe? Wenn ich nicht so klug bin, darf ich dann kein solches Refactoring durchführen? Muss ich meinen Code rauswerfen und von vorne beginnen?Antworten:
Was Sie fragen, ist, wo Sie platzieren sollen
b()
, und in einem anderen Sinne ist eine Frage, obA
die beste Wahl als unmittelbare Superklasse fürAB
und istAC
.Es scheint drei Möglichkeiten zu geben:
b()
in beidenAB
undAC
ABAC-Parent
, die von erbtA
und diese einführtb()
und dann als unmittelbare Superklasse fürAB
und verwendet wirdAC
b()
inA
(nicht zu wissen , ob eine andere Zukunft KlasseAD
wollenb()
oder nicht)Bis sich eine andere Klasse
AD
, die nicht will,b()
präsentiert, scheint (3) die richtige Wahl zu sein.Zum Zeitpunkt eines
AD
Geschenks können wir dann den Ansatz in (2) umgestalten - schließlich handelt es sich um Software!quelle
b()
keine Klasse ein. Machen Sie es zu einer freien Funktion, die alle ihre Daten als Argumente verwendet und beides hatAB
und sieAC
aufruft. Dies bedeutet, dass Sie es beim Hinzufügen nicht verschieben oder weitere Klassen erstellen müssenAD
.b()
kein instanzlanglebiger Zustand benötigt wird.ac
, die aufruft,b
anstattac
abstrakt zu sein?Eine abstrakte Klasse ist nicht als Abladeplatz für verschiedene Funktionen oder Daten gedacht, die in die abstrakte Klasse geworfen werden, da dies praktisch ist.
Die einzige Faustregel, die den zuverlässigsten und erweiterbarsten objektorientierten Ansatz zu bieten scheint, lautet: " Bevorzugen Sie die Komposition gegenüber der Vererbung ." Eine abstrakte Klasse wird wahrscheinlich am besten als Schnittstellenspezifikation angesehen, die keinen Code enthält.
Wenn Sie eine Methode haben, die eine Art Bibliotheksmethode ist, die mit der abstrakten Klasse einhergeht, eine Methode, die das wahrscheinlichste Mittel ist, um Funktionen oder Verhaltensweisen auszudrücken, die Klassen, die von einer abstrakten Klasse abgeleitet sind, normalerweise benötigen, ist es sinnvoll, eine zu erstellen Klasse zwischen der abstrakten Klasse und anderen Klassen, in denen diese Methode verfügbar ist. Diese neue Klasse bietet eine bestimmte Instanziierung der abstrakten Klasse, die eine bestimmte Implementierungsalternative oder einen bestimmten Implementierungspfad definiert, indem diese Methode bereitgestellt wird.
Die Idee einer abstrakten Klasse besteht darin, ein abstraktes Modell dessen zu haben, was die abgeleiteten Klassen, die die abstrakte Klasse tatsächlich implementieren, als Dienst oder Verhalten bereitstellen sollen. Vererbung ist leicht zu überbeanspruchen und so oft bestehen die nützlichsten Klassen aus verschiedenen Klassen unter Verwendung eines Mischmusters .
Es gibt jedoch immer die Änderungsfragen, was sich ändern wird und wie es sich ändern wird und warum es sich ändern wird.
Vererbung kann zu spröden und zerbrechlichen Quellen führen (siehe auch Vererbung: Verwenden Sie sie einfach nicht mehr! ).
quelle
b()
eine reine Funktion ist. Es könnte ein Funktoid oder eine Vorlage oder etwas anderes sein. Ich verwende den Ausdruck "Bibliotheksmethode" als eine Komponente, ob reine Funktion, COM-Objekt oder was auch immer, das für die Funktionalität verwendet wird, die es für die Lösung bereitstellt.Ihre Frage schlägt einen Entweder-Oder-Ansatz für abstrakte Klassen vor. Aber ich denke, dass Sie sie als ein weiteres Werkzeug in Ihrer Toolbox betrachten sollten. Und dann stellt sich die Frage: Für welche Jobs / Probleme sind abstrakte Klassen das richtige Werkzeug?
Ein ausgezeichneter Anwendungsfall ist die Implementierung des Template-Methodenmusters . Sie fügen die gesamte invariante Logik in die abstrakte Klasse und die Variantenlogik in ihre Unterklassen ein. Beachten Sie, dass die gemeinsam genutzte Logik an sich unvollständig und nicht funktionsfähig ist. Meistens geht es darum, einen Algorithmus zu implementieren, bei dem mehrere Schritte immer gleich sind, aber mindestens ein Schritt variiert. Setzen Sie diesen einen Schritt als abstrakte Methode ein, die von einer der Funktionen innerhalb der abstrakten Klasse aufgerufen wird.
Ich denke, Ihr erstes Beispiel ist im Wesentlichen eine Beschreibung des Musters der Vorlagenmethode (korrigieren Sie mich, wenn ich falsch liege), daher würde ich dies als einen vollkommen gültigen Anwendungsfall für abstrakte Klassen betrachten.
Für Ihr zweites Beispiel denke ich, dass die Verwendung abstrakter Klassen nicht die optimale Wahl ist, da es überlegene Methoden für den Umgang mit gemeinsam genutzter Logik und dupliziertem Code gibt. Angenommen, Sie haben eine abstrakte Klasse
A
, abgeleitete KlassenB
undC
beide abgeleiteten Klassen teilen eine Logik in Form einer Methodes()
. Um den richtigen Ansatz zu finden, um die Duplizierung zu beseitigen, ist es wichtig zu wissen, ob die Methodes()
Teil der öffentlichen Schnittstelle ist oder nicht.Wenn dies nicht
b()
der Fall ist (wie in Ihrem eigenen konkreten Beispiel mit Methode ), ist der Fall recht einfach. Erstellen Sie einfach eine separate Klasse daraus und extrahieren Sie den Kontext, der zum Ausführen der Operation erforderlich ist. Dies ist ein klassisches Beispiel für Komposition über Vererbung . Wenn wenig oder kein Kontext benötigt wird, reicht möglicherweise bereits eine einfache Hilfsfunktion aus, wie in einigen Kommentaren vorgeschlagen.Wenn
s()
es Teil der öffentlichen Schnittstelle ist, wird es etwas schwieriger. Unter der Annahme, dasss()
nichts damit zu tun hatB
undC
damit verwandt istA
, sollten Sie nichts()
hineinsteckenA
. Wo soll es dann hingelegt werden? Ich würde dafür argumentieren, eine separate Schnittstelle zu deklarierenI
, die definierts()
. Andererseits sollten Sie eine separate Klasse erstellen, die die Logik für die Implementierungs()
und beides enthältB
und davonC
abhängt.Zuletzt finden Sie hier einen Link zu einer hervorragenden Antwort auf eine interessante Frage zu SO, die Ihnen bei der Entscheidung helfen kann, wann Sie sich für eine Schnittstelle und wann für eine abstrakte Klasse entscheiden:
quelle
Mir scheint, Sie vermissen den Punkt der Objektorientierung, sowohl logisch als auch technisch. Sie beschreiben zwei Szenarien: Gruppieren des allgemeinen Verhaltens von Typen in einer Basisklasse und Polymorphismus. Dies sind beide legitime Anwendungen einer abstrakten Klasse. Ob Sie die Klasse abstrakt machen sollten oder nicht, hängt jedoch von Ihrem analytischen Modell ab, nicht von den technischen Möglichkeiten.
Sie sollten einen Typ erkennen, der in der realen Welt keine Inkarnationen hat, aber die Grundlage für bestimmte Typen bildet, die existieren. Beispiel: ein Tier. Es gibt kein Tier. Es ist immer ein Hund oder eine Katze oder was auch immer, aber es gibt kein echtes Tier. Doch Animal umrahmt sie alle.
Dann sprichst du von Levels. Levels sind nicht Teil des Geschäfts, wenn es um Vererbung geht. Auch das Erkennen gemeinsamer Daten oder gemeinsamen Verhaltens ist kein technischer Ansatz, der wahrscheinlich nicht helfen wird. Sie sollten ein Stereotyp erkennen und dann die Basisklasse einfügen. Wenn es kein solches Stereotyp gibt, sind einige Schnittstellen, die von mehreren Klassen implementiert werden, möglicherweise besser geeignet.
Der Name Ihrer abstrakten Klasse zusammen mit ihren Mitgliedern sollte sinnvoll sein. Es muss sowohl technisch als auch logisch unabhängig von abgeleiteten Klassen sein. Wie Animal könnte eine abstrakte Methode Eat (die polymorph wäre) und boolesche abstrakte Eigenschaften IsPet und IsLifestock haben, die Sinn macht, ohne über Katzen, Hunde oder Schweine Bescheid zu wissen. Jede Abhängigkeit (technisch oder logisch) sollte nur in eine Richtung gehen: vom Nachkommen zur Basis. Basisklassen selbst sollten auch keine Kenntnisse über absteigende Klassen haben.
quelle
Erster Fall aus Ihrer Frage:
Zweiter Fall:
Dann Ihre Frage :
IMO haben Sie zwei verschiedene Szenarien, daher können Sie keine einzige Regel zeichnen, um zu entscheiden, welches Design angewendet werden muss.
Für den ersten Fall haben wir Dinge wie das Muster der Vorlagenmethode , die bereits in anderen Antworten erwähnt wurden.
Für den zweiten Fall haben Sie das Beispiel einer abstrakten Klasse A angegeben, die die ab () -Methode empfängt. IMO sollte die b () -Methode nur verschoben werden, wenn dies für ALLE anderen abgeleiteten Klassen sinnvoll ist. Mit anderen Worten, wenn Sie es verschieben, weil es an zwei Stellen verwendet wird, ist dies wahrscheinlich keine gute Wahl, da es morgen möglicherweise eine neue konkrete abgeleitete Klasse gibt, für die b () überhaupt keinen Sinn ergibt. Wie Sie bereits sagten, ist es wahrscheinlich auch ein Hinweis darauf, dass dies keine gute Wahl für das Design ist, wenn Sie die Klasse A separat betrachten und b () in diesem Zusammenhang ebenfalls keinen Sinn ergibt.
quelle
Ich habe vor einiger Zeit genau die gleichen Fragen gehabt!
Wenn ich auf dieses Problem stoße, stelle ich fest, dass es ein verstecktes Konzept gibt, das ich nicht gefunden habe. Und dieses Konzept sollte höchstwahrscheinlich mit einem Wertobjekt ausgedrückt werden . Sie haben ein sehr abstraktes Beispiel, daher kann ich leider nicht demonstrieren, was ich mit Ihrem Code meine. Aber hier ist ein Fall aus meiner eigenen Praxis. Ich hatte zwei Klassen, die eine Möglichkeit darstellten, eine Anfrage an eine externe Ressource zu senden und die Antwort zu analysieren. Es gab Ähnlichkeiten darin, wie die Anfragen gebildet wurden und wie die Antworten analysiert wurden. So sah meine Hierarchie aus:
Ein solcher Code zeigt an, dass ich die Vererbung einfach missbrauche. So könnte es umgestaltet werden:
Seitdem behandle ich die Erbschaft sehr sorgfältig, weil ich viele Male verletzt wurde.
Seitdem bin ich zu einem anderen Schluss über die Vererbung gekommen. Ich bin stark gegen Vererbung aufgrund der internen Struktur . Es bricht die Kapselung, es ist zerbrechlich, es ist schließlich prozedural. Und wenn ich meine Domain richtig zerlege , kommt die Vererbung einfach nicht sehr oft vor.
quelle