Ich habe eine Basisklasse Base
. Es hat zwei Unterklassen Sub1
und Sub2
. Jede Unterklasse verfügt über einige zusätzliche Methoden. Zum Beispiel Sub1
hat Sandwich makeASandwich(Ingredients... ingredients)
und Sub2
hat boolean contactAliens(Frequency onFrequency)
.
Da diese Methoden unterschiedliche Parameter verwenden und völlig unterschiedliche Aufgaben ausführen, sind sie vollständig inkompatibel, und ich kann nicht einfach den Polymorphismus verwenden, um dieses Problem zu lösen.
Base
bietet den größten Teil der Funktionalität, und ich habe eine große Sammlung von Base
Objekten. Alle Base
Objekte sind jedoch entweder ein Sub1
oder ein Sub2
, und manchmal muss ich wissen, um welche es sich handelt.
Es scheint eine schlechte Idee zu sein, Folgendes zu tun:
for (Base base : bases) {
if (base instanceof Sub1) {
((Sub1) base).makeASandwich(getRandomIngredients());
// ... etc.
} else { // must be Sub2
((Sub2) base).contactAliens(getFrequency());
// ... etc.
}
}
Also habe ich mir eine Strategie ausgedacht, um dies zu vermeiden, ohne zu zaubern. Base
Jetzt hat diese Methoden:
boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();
Und natürlich Sub1
implementiert diese Methoden als
boolean isSub1() { return true; }
Sub1 asSub1(); { return this; }
Sub2 asSub2(); { throw new IllegalStateException(); }
Und Sub2
setzt sie umgekehrt um.
Leider haben jetzt Sub1
und Sub2
diese Methoden eine eigene API. So kann ich das zum Beispiel weiter machen Sub1
.
/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1(); { return this; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2(); { throw new IllegalStateException(); }
Wenn bekannt ist, dass das Objekt nur ein Objekt ist Base
, sind diese Methoden nicht veraltet und können verwendet werden, um sich selbst in einen anderen Typ umzuwandeln, sodass ich die Methoden der Unterklasse aufrufen kann. In gewisser Weise erscheint mir das elegant, aber andererseits missbrauche ich veraltete Annotationen, um Methoden aus einer Klasse zu "entfernen".
Da eine Sub1
Instanz wirklich eine Basis ist, ist es sinnvoll, Vererbung anstelle von Kapselung zu verwenden. Geht es mir gut? Gibt es einen besseren Weg, um dieses Problem zu lösen?
quelle
instanceof
sind auf eine Weise, die viel Eingabe erfordert, fehleranfällig und machen es schwierig, weitere Unterklassen hinzuzufügen.Sub1
undSub2
kann nicht austauschbar verwendet werden, warum behandeln Sie sie dann als solche? Warum nicht Ihre "Sandwichmacher" und "Außerirdischen" getrennt nachverfolgen?Antworten:
Es ist nicht immer sinnvoll, der Basisklasse Funktionen hinzuzufügen, wie in einigen anderen Antworten vorgeschlagen. Das Hinzufügen zu vieler Sonderfallfunktionen kann dazu führen, dass ansonsten nicht miteinander verknüpfte Komponenten miteinander verbunden werden.
Zum Beispiel könnte ich eine
Animal
Klasse mitCat
undDog
Komponenten haben. Wenn ich in der Lage sein wollen , dass sie zu drucken, oder zeigen Sie sie in der GUI könnte es übertrieben für mich sein , um hinzuzufügenrenderToGUI(...)
undsendToPrinter(...)
zu der Basisklasse.Der Ansatz, den Sie mit Hilfe von Typprüfungen und Abgüssen verfolgen, ist fragil - zumindest werden die Bedenken jedoch getrennt.
Wenn Sie jedoch feststellen, dass Sie diese Arten von Überprüfungen / Casts häufig durchführen, besteht eine Option darin, das Besucher- / Doppelversandmuster für diese zu implementieren. Es sieht ungefähr so aus:
Jetzt wird Ihr Code
Der Hauptvorteil besteht darin, dass beim Hinzufügen einer Unterklasse Kompilierungsfehler auftreten und keine Fälle im Hintergrund fehlen. Die neue Besucherklasse wird auch zu einem netten Ziel, in das Funktionen hineingezogen werden. Zum Beispiel könnte es Sinn machen , sich zu bewegen
getRandomIngredients()
inActOnBase
.Sie können auch die Schleifenlogik extrahieren: Beispielsweise könnte das obige Fragment zu werden
Ein wenig weiter massieren und mit Java 8 Lambdas und Streaming können Sie bekommen
Welche IMO ist so gut aussehend und prägnant, wie Sie bekommen können.
Hier ist ein vollständigeres Java 8-Beispiel:
quelle
Sub3
. Hat der Besucher nicht genau das gleiche Problem wieinstanceof
? Jetzt müssen SieonSub3
den Besucher hinzufügen und es bricht, wenn Sie vergessen.onSub3
nach dem Hinzufügen zur Besucheroberfläche an jedem Ort, an dem Sie einen neuen Besucher erstellen, ein Kompilierungsfehler auftritt, der noch nicht aktualisiert wurde. Im Gegensatz dazu führt das Einschalten bestenfalls zu einem Laufzeitfehler - der schwieriger auszulösen sein kann.Aus meiner Sicht: Ihr Design ist falsch .
In die natürliche Sprache übersetzt, sagen Sie Folgendes:
Vorausgesetzt wir haben
animals
, gibt escats
undfish
.animals
haben Eigenschaften, die gemeinsam sindcats
undfish
. Aber das reicht nicht aus: Es gibt einige Eigenschaften, die sichcat
von denen unterscheidenfish
, daher müssen Sie Unterklassen bilden.Jetzt haben Sie das Problem, dass Sie vergessen haben, Bewegung zu modellieren . Okay. Das ist relativ einfach:
Das ist aber ein falsches Design. Der richtige Weg wäre:
Wo
move
würde geteiltes Verhalten von jedem Tier anders umgesetzt werden.Das heißt: Ihr Design ist kaputt.
Meine Empfehlung: Refactoring
Base
,Sub1
undSub2
.quelle
.move()
oder.attack()
und richtig abstrakterthat
Teil - anstelle von.swim()
,.fly()
,.slide()
,.hop()
,.jump()
,.squirm()
,.phone_home()
etc. Wie Refactoring Sie richtig? Unbekannt ohne bessere Beispiele ... aber im Allgemeinen immer noch die richtige Antwort - es sei denn, Beispielcode und weitere Details legen etwas anderes nahe.move
vsfly
/run
/ etc, das kann in einem Satz erklärt werden als: Sie sollten dem Objekt sagen, was zu tun ist, nicht wie es zu tun ist. In Bezug auf die Zugriffsmethoden sollten Sie, wie Sie in einem Kommentar an dieser Stelle bereits erwähnt haben, dem Objekt Fragen stellen und nicht dessen Status überprüfen.Es ist ein wenig schwierig, sich einen Umstand vorzustellen, in dem Sie eine Gruppe von Dingen haben und möchten, dass sie entweder ein Sandwich machen oder Aliens kontaktieren. In den meisten Fällen, in denen Sie ein solches Casting finden, werden Sie mit einem Typ arbeiten - z. B. filtern Sie in clang eine Reihe von Knoten nach Deklarationen, bei denen getAsFunction nicht null zurückgibt, anstatt für jeden Knoten in der Liste etwas anderes zu tun.
Möglicherweise müssen Sie eine Aktionssequenz ausführen, und es ist nicht relevant, ob die Objekte, die die Aktion ausführen, miteinander verknüpft sind.
Base
Arbeiten Sie also anstelle einer Liste von an der Liste der Aktionenwoher
Gegebenenfalls kann Base eine Methode implementieren, um die Aktion zurückzugeben, oder eine andere Methode, mit der Sie die Aktion aus den Instanzen in Ihrer Basisliste auswählen müssen (da Sie sagen, dass Sie keinen Polymorphismus verwenden können, vermutlich also die auszuführende Aktion) ist keine Funktion der Klasse, sondern eine andere Eigenschaft der Basen, ansonsten würden Sie Base nur die act (Context) -Methode geben.
quelle
Context
Objekt macht die Sache nur noch schlimmer. Ich könnte genauso gutObject
s an die Methoden weitergeben, sie besetzen und hoffen, dass sie der richtige Typ sind.Context
es sich nicht unbedingt um eine neue Klasse oder gar eine Schnittstelle handeln muss. Normalerweise ist der Kontext nur der Anrufer, sodass Anrufe in der Form erfolgenaction.act(this)
. WenngetFrequency()
undgetRandomIngredients()
statische Methoden sind, benötigen Sie möglicherweise nicht einmal einen Kontext. So würde ich beispielsweise eine Job-Warteschlange implementieren, nach der sich Ihr Problem fürchterlich anhört.Wie wäre es, wenn Ihre Unterklassen eine oder mehrere Schnittstellen implementieren, die definieren, was sie tun können? Etwas wie das:
Dann sehen deine Klassen so aus:
Und Ihre for-Schleife wird:
Zwei Hauptvorteile dieses Ansatzes:
PS: Entschuldigung für die Namen der Interfaces, mir fiel in diesem Moment nichts Cooleres ein: D.
quelle
Der Ansatz kann in Fällen sinnvoll sein, in denen fast jeder Typ innerhalb einer Familie entweder direkt als Implementierung einer Schnittstelle verwendet werden kann, die ein bestimmtes Kriterium erfüllt, oder zur Erstellung einer Implementierung dieser Schnittstelle verwendet werden kann. Die eingebauten Sammlungstypen hätten IMHO von diesem Muster profitiert, aber da sie nicht zu Beispielzwecken dienen, werde ich eine Sammlungsschnittstelle erfinden
BunchOfThings<T>
.Einige Implementierungen von
BunchOfThings
sind veränderlich. manche nicht. In vielen Fällen möchte ein Objekt, das Fred verwenden kann, möglicherweise etwas enthalten,BunchOfThings
und er weiß, dass nichts anderes als Fred in der Lage ist, es zu ändern. Diese Anforderung kann auf zwei Arten erfüllt werden:Fred weiß, dass es die einzigen Verweise darauf enthält
BunchOfThings
, und dassBunchOfThings
es nirgendwo im Universum externe Verweise darauf gibt. Wenn niemand einen Verweis auf dasBunchOfThings
oder seine Interna hat, kann ihn niemand ändern, sodass die Einschränkung erfüllt wird.Weder die
BunchOfThings
noch irgendwelche ihrer Interna, auf die externe Verweise existieren, können auf irgendeine Weise modifiziert werden. Wenn absolut niemand a ändern kannBunchOfThings
, ist die Bedingung erfüllt.Eine Möglichkeit, die Einschränkung zu erfüllen, besteht darin, ein empfangenes Objekt bedingungslos zu kopieren (verschachtelte Komponenten rekursiv zu verarbeiten). Eine andere Möglichkeit wäre, zu testen, ob ein empfangenes Objekt Unveränderlichkeit verspricht, und, falls nicht, eine Kopie davon anzufertigen und dies auch mit verschachtelten Komponenten zu tun. Eine Alternative, die in der Regel sauberer als die zweite und schneller als die erste ist, besteht darin, eine
AsImmutable
Methode anzubieten , mit der ein Objekt aufgefordert wird, eine unveränderliche Kopie von sich selbst zu erstellen (unter VerwendungAsImmutable
aller geschachtelten Komponenten, die dies unterstützen).Verwandte Methoden können ebenfalls bereitgestellt werden
asDetached
(für die Verwendung, wenn Code ein Objekt empfängt und nicht weiß, ob es mutiert werden soll). In diesem Fall sollte ein mutierbares Objekt durch ein neues mutierbares Objekt ersetzt werden, aber ein unveränderliches Objekt kann beibehalten werden as-is),asMutable
(in Fällen , in denen ein Objekt weiß, dass es ein zuvor zurückgegebenes Objekt enthältasDetached
, dh entweder eine nicht gemeinsam genutzte Referenz auf ein veränderbares Objekt oder eine gemeinsam nutzbare Referenz auf ein veränderbares Objekt) undasNewMutable
(in Fällen, in denen der Code ein Äußeres empfängt Referenz und weiß, dass eine Kopie der darin enthaltenen Daten mutiert werden soll. Wenn die eingehenden Daten mutierbar sind, gibt es keinen Grund, eine unveränderliche Kopie anzufertigen, die sofort zum Erstellen einer mutierbaren Kopie verwendet und dann verworfen wird.Beachten Sie, dass die
asXX
Methoden zwar leicht unterschiedliche Typen zurückgeben können, ihre eigentliche Rolle jedoch darin besteht, sicherzustellen, dass die zurückgegebenen Objekte die Programmanforderungen erfüllen.quelle
Wenn ich die Frage ignoriere, ob Sie ein gutes Design haben oder nicht, und annehme, dass es gut oder zumindest akzeptabel ist, möchte ich die Fähigkeiten der Unterklassen berücksichtigen, nicht den Typ.
Daher entweder:
Verschieben Sie einige Kenntnisse über die Existenz von Sandwiches und Aliens in die Basisklasse, obwohl Sie wissen, dass einige Instanzen der Basisklasse dies nicht können. Implementieren Sie es in der Basisklasse, um Ausnahmen auszulösen, und ändern Sie den Code in:
Dann überschreibt eine oder beide Unterklassen
canMakeASandwich()
und nur eine implementiert jedes vonmakeASandwich()
undcontactAliens()
.Verwenden Sie keine konkreten Unterklassen, sondern Schnittstellen, um festzustellen, über welche Funktionen ein Typ verfügt. Lassen Sie die Basisklasse in Ruhe und ändern Sie den Code in:
oder möglicherweise (und zögern Sie nicht, diese Option zu ignorieren, wenn sie nicht zu Ihrem Stil oder zu einem für Sie vernünftigen Java-Stil passt):
Persönlich mag ich die letztere Art, eine halb erwartete Ausnahme zu erwischen, normalerweise nicht , weil die Gefahr besteht, dass ein
ClassCastException
Coming-out vongetRandomIngredients
odermakeASandwich
nicht richtig erkannt wird, aber YMMV.quelle
<
ist, das Fangen zu vermeidenArrayIndexOutOfBoundsException
, und einige Leute entscheiden sich auch dafür. Keine Berücksichtigung des Geschmacks ;-) Grundsätzlich ist mein "Problem" hier, dass ich in Python arbeite, wobei der bevorzugte Stil darin besteht, dass das Abfangen einer Ausnahme dem Testen im Voraus vorzuziehen ist, ob die Ausnahme auftritt, egal wie einfach der Voraus-Test wäre . Vielleicht ist die Möglichkeit in Java einfach nicht erwähnenswert.Hier haben wir einen interessanten Fall einer Basisklasse, die sich in ihre eigenen abgeleiteten Klassen zurückwirft. Wir wissen genau, dass dies normalerweise schlecht ist, aber wenn wir sagen möchten, dass wir einen guten Grund gefunden haben, lassen Sie uns schauen und sehen, welche Einschränkungen dies haben könnte:
Wenn 4 dann haben wir: 5. Die Politik der abgeleiteten Klasse steht immer unter der gleichen politischen Kontrolle wie die Politik der Basisklasse.
2 und 5 implizieren beide direkt, dass wir alle abgeleiteten Klassen auflisten können, was bedeutet, dass es keine externen abgeleiteten Klassen geben soll.
Aber hier ist das Ding. Wenn sie ganz bei Ihnen sind, können Sie das if durch eine Abstraktion ersetzen, die ein virtueller Methodenaufruf ist (auch wenn es ein Unsinn ist), und das if und die Selbstdarstellung entfernen. Deshalb tu es nicht. Ein besseres Design ist verfügbar.
quelle
Fügen Sie eine abstrakte Methode in die Basisklasse ein, die eines in Sub1 und das andere in Sub2 ausführt, und rufen Sie diese abstrakte Methode in Ihren gemischten Schleifen auf.
Beachten Sie, dass dies abhängig von der Definition von getRandomIngredients und getFrequency andere Änderungen erfordern kann. Aber ehrlich gesagt, in der Klasse, in der Aliens kontaktiert werden, ist getFrequency wahrscheinlich sowieso besser zu definieren.
Übrigens sind Ihre asSub1- und asSub2-Methoden definitiv eine schlechte Übung. Wenn Sie dies tun, tun Sie es durch Casting. Diese Methoden bieten Ihnen nichts, was Sie beim Casting nicht kennen.
quelle
Sie können beide
makeASandwich
undcontactAliens
in die Basisklasse verschieben und sie dann mit Dummy-Implementierungen implementieren. Da die Rückgabetypen / Argumente nicht in eine gemeinsame Basisklasse aufgelöst werden können.Es gibt offensichtliche Nachteile dieser Art von Dingen, wie was dies für den Vertrag der Methode bedeutet und was Sie über den Kontext des Ergebnisses ableiten können und was nicht. Du kannst nicht denken, da ich es versäumt habe, ein Sandwich zu machen. Die Zutaten sind bei dem Versuch aufgebraucht.
quelle
Was Sie tun, ist absolut legitim. Achten Sie nicht auf die Neinsager, die nur das Dogma wiederholen, weil sie es in einigen Büchern lesen. Das Dogma hat keinen Platz in der Technik.
Ich habe den gleichen Mechanismus ein paarmal angewendet und kann mit Sicherheit sagen, dass die Java-Laufzeit an mindestens einer mir in den Sinn gekommenen Stelle das Gleiche hätte tun können, um so die Leistung, Benutzerfreundlichkeit und Lesbarkeit von Code zu verbessern benutzt es.
Nehmen wir zum Beispiel
java.lang.reflect.Member
, welches die Basis vonjava.lang.reflect.Field
und istjava.lang.reflect.Method
. (Die tatsächliche Hierarchie ist etwas komplizierter, aber das ist irrelevant.) Felder und Methoden sind sehr unterschiedliche Tiere: Eines hat einen Wert, den Sie erhalten oder festlegen können, während das andere keinen solchen Wert hat, der jedoch aufgerufen werden kann eine Reihe von Parametern und es kann einen Wert zurückgeben. Felder und Methoden sind also beide Mitglieder, aber die Dinge, die Sie mit ihnen tun können, unterscheiden sich ungefähr so stark voneinander wie das Herstellen von Sandwiches oder das Kontaktieren von Aliens.Wenn wir nun Code schreiben, der Reflektion verwendet, haben wir sehr oft einen
Member
in unseren Händen, und wir wissen, dass es entweder einMethod
oder einField
(oder selten etwas anderes) ist, und dennoch müssen wir alles Mühsame tuninstanceof
, um genau herauszufinden was es ist und dann müssen wir es besetzen, um einen richtigen Verweis darauf zu bekommen. (Und das ist nicht nur mühsam, sondern auch nicht sehr leistungsfähig.) DieMethod
Klasse hätte das von Ihnen beschriebene Muster sehr einfach implementieren können, wodurch das Leben Tausender Programmierer erleichtert wurde.Natürlich ist diese Technik nur in kleinen, genau definierten Hierarchien eng gekoppelter Klassen praktikabel, über die Sie auf Quellenebene verfügen (und die Sie immer haben werden): Sie möchten so etwas nicht tun, wenn Ihre Klassenhierarchie dies ist von Personen, die nicht die Freiheit haben, die Basisklasse umzugestalten, verlängert werden können.
So unterscheidet sich das, was ich getan habe, von dem, was Sie getan haben:
Die Basisklasse stellt eine Standardimplementierung für die gesamte
asDerivedClass()
Methodenfamilie bereit , wobei jede einzelne davon zurückgegeben wirdnull
.Jede abgeleitete Klasse überschreibt nur eine der
asDerivedClass()
Methoden und gibtthis
stattdessen zurücknull
. Es setzt nichts von alledem außer Kraft und möchte auch nichts über sie wissen. Also werden keineIllegalStateException
s geworfen.Die Basisklasse stellt auch
final
Implementierungen für die gesamteisDerivedClass()
Methodenfamilie bereit , die wie folgt codiert sind: Aufreturn asDerivedClass() != null;
diese Weise wird die Anzahl der Methoden, die von abgeleiteten Klassen überschrieben werden müssen, minimiert.Ich habe
@Deprecated
diesen Mechanismus nicht benutzt , weil ich nicht daran gedacht habe. Nun, da Sie mir die Idee gegeben haben, werde ich sie nutzen, danke!In C # ist ein ähnlicher Mechanismus über das
as
Schlüsselwort integriert. In C # können Sie sagen, dassDerivedClass derivedInstance = baseInstance as DerivedClass
Sie einen Verweis auf a erhalten,DerivedClass
ob SiebaseInstance
zu dieser Klasse gehörten odernull
nicht. Dies führt (theoretisch) zu einer besseren Leistung alsis
das von cast verfolgte (is
ist das zugegebenermaßen besser benannte C # -Schlüsselwort fürinstanceof
), aber der benutzerdefinierte Mechanismus, den wir von Hand erstellt haben, funktioniert noch besser: das Paarinstanceof
-und-cast-Operationen von Java ebenfalls Da deras
Operator von C # nicht so schnell ist wie der einzelne virtuelle Methodenaufruf unseres benutzerdefinierten Ansatzes.Ich mache hiermit den Vorschlag, dass diese Technik als solche deklariert werden sollte als Muster und einen schönen Namen dafür zu finden.
Danke für die Ablehnung!
Eine Zusammenfassung der Kontroverse, um Ihnen das Lesen der Kommentare zu ersparen:
Der Einwand der Leute scheint zu sein, dass das ursprüngliche Design falsch war, was bedeutet, dass Sie niemals sehr unterschiedliche Klassen haben sollten, die von einer gemeinsamen Basisklasse abgeleitet sind, oder dass selbst wenn Sie dies tun, der Code, der eine solche Hierarchie verwendet, niemals in der Lage sein sollte, zu haben eine Basisreferenz und müssen die abgeleitete Klasse herausfinden. Sie sagen daher, dass der von dieser Frage und meiner Antwort vorgeschlagene Mechanismus des Selbstwerfens, der die Verwendung des ursprünglichen Entwurfs verbessert, niemals an erster Stelle hätte notwendig sein dürfen. (Sie sagen eigentlich nichts über den Mechanismus des Selbstwerfens selbst aus, sondern beschweren sich nur über die Art der Entwürfe, auf die der Mechanismus angewendet werden soll.)
Allerdings oben ich in dem Beispiel hat bereits gezeigt , dass die Schöpfer der Java - Laufzeit in der Tat wählte genau so einen Entwurf für die
java.lang.reflect.Member
,Field
,Method
Hierarchie, und in den Kommentaren unten ich auch zeigen , dass die Schöpfer der C # Laufzeit unabhängig in einer angekommen Äquivalent Design für dieSystem.Reflection.MemberInfo
,FieldInfo
,MethodInfo
Hierarchie. Das sind also zwei verschiedene reale Szenarien, die jedem unter die Nase fallen und die nachweislich praktikable Lösungen für genau solche Entwürfe bieten.Darauf laufen alle folgenden Kommentare hinaus. Der Selbstgussmechanismus wird kaum erwähnt.
quelle
java.lang.reflection.Member
Hierarchie besteht. Die Entwickler der Java-Laufzeitumgebung waren in einer solchen Situation, ich nehme an, Sie würden nicht sagen, dass sie sich selbst in eine Box verwandelt haben, oder beschuldigen Sie sie, keine Best Practices befolgt zu haben? Der Punkt, den ich hier anspreche, ist, dass, sobald Sie diese Art von Situation haben, dieser Mechanismus eine gute Möglichkeit ist, Dinge zu verbessern.Member
Schnittstelle, die jedoch nur zur Überprüfung der Zugriffsberechtigungen dient. In der Regel erhalten Sie eine Liste von Methoden oder Eigenschaften, die Sie direkt verwenden (ohne sie zu mischen, daher wird "no"List<Member>
angezeigt), sodass Sie keine Umwandlung vornehmen müssen. Beachten Sie, dassClass
keinegetMembers()
Methode bereitgestellt wird. Natürlich könnte ein ahnungsloser Programmierer das erstellenList<Member>
, aber das würde genauso wenig Sinn machen, als es zu einem zu machenList<Object>
und einiges hinzuzufügenInteger
oder esString
zu ergänzen.null
anstelle einer Ausnahme zurückzukehren, macht es nicht viel besser. Wenn alles andere gleich ist, ist der Besucher bei der gleichen Arbeit sicherer.