Chromanoid, sehe ich ähnlich, wenn ich es richtig verstanden habe, siehe meine Antwort im Folgenden.
dot hat geschrieben:In dem Moment, wo du einen Downcast brauchst, befindest du dich in einer Situation, in der dein Design sich selbst widerspricht. Der fragliche Code hat von seinem Aufrufer nur einen X Basisklassenzeiger bekommen, was bedeutet, dass das Interface des fraglichen Code sagt: Ich mache dieses und jenes mit jedem beliebigen X. Wenn du in der Implementierung dieses Code dann auf einmal ein konkretes Y brauchst, dann sagt uns das, dass der entsprechende Code offenbar doch nicht mit jedem beliebigen X funktioniert, sondern nur mit jenen X, die ein Y sind. Entweder hätte also das Interface bereits nach einem Y verlangen sollen, da die entsprechende Operation offenbar nur mit einem Y Sinn macht und nicht mit jedem X im Allgemeinen, oder irgendwas ist mit der Abstraktion X an sich verkehrt. Die Lösung ist in jeden Fall, das Problem im Design, das zu der entsprechenden Situation geführt hat, zu beheben und nicht – per Downcast – Code zu schreiben, der so tut, als würde er für jedes X das selbe machen, in Wirklichkeit aber, je nachdem was für ein konkretes Objekt sich hinter dem jeweiligen X befindet, etwas anderes macht...
Das ist die Art von Antwort, die ich meinte: Das Design ist schlecht, weil ein solches Design schlecht ist. Werd doch mal konkreter :)
Im Prinzip habe ich eine Kollisionsmethode, die nur X benötigt und nach außen hin mit jedem X funktioniert. Die Information welches Y das X ist, ist auch vorhanden, nicht in Form der sprachtechnischen Ausprägung als Basisklasse, sondern gewissermaßen als Member, die jedes X besitzt. Ich denke das geht in die Richtung, die
Chromanoid angesprochen hatte.
dot hat geschrieben: Dein konkreter Fall klingt nach einem Paradebeispiel für Double Dispatch... ;)
dot hat geschrieben:Es geht hier nicht um Casts im Allgemeinen, sondern konkret um Downcasts (d.h. Cast von Basis runter auf abgeleitete Klasse). Ich bin absolut kein Fan von Absolutismen ;). Aber wenn es einen Fall gibt, wo man wirklich mal eine absolute Aussage machen kann, dann ist es: Ein Downcast ist immer Symptom eines Designfehlers. In all meinen Jahren ist mir noch nicht ein einziges Beispiel untergekommen, wo man imo auch nur beginnen hätte können, darüber nachzudenken, ob man vielleicht diskutieren könnte, dass ein Downcast unter gewissen Voraussetzungen nicht eventuell doch vertretbar sein könnte. Selbst nach vielmaligem, intensivem Nachdenken konnte ich bis jetzt noch nicht mal ein rein hypothetisches Beispiel für den potentiell akzeptablen Einsatz eines Downcast erfinden. Ich lasse mich natürlich immer gerne eines Besseren belehren, aber ich hab diese Diskussion schon wirklich oft geführt und der Downcast steht bei mir immer noch ganz weit oben auf der Liste der ganz groben Vergehen, gleich neben so Dingen wie dem Singleton Pattern...
Siehe oben, werd doch mal konkret :) . Was ist am obigen Beispiel der Kollisionsabfrage so schlimm, welche Fallstricke erwarten mich? Ich habe eine definierte Stelle an der ich weiß, dass Objekte ankommen, die auf Kollision geprüft werden und alle ihren Typ mit sich bringen. Nochmal darauf speziell:
dot hat geschrieben:Selbst nach vielmaligem, intensivem Nachdenken konnte ich bis jetzt noch nicht mal ein rein hypothetisches Beispiel für den potentiell akzeptablen Einsatz eines Downcast erfinden.
Deine Kernaussage es sei grundsätzlich schlecht, weil es schlecht ist, klingt für mich viel hypothetischer und diskutiert habe ich auch schon oft. ;)
Zur Kopplung:
dot hat geschrieben: Inwiefern war welche Kopplung zu stark? Dass die Kollisionsbestimmung zwischen verschiedenen Kombinationen verschiedener Objekttypen jeweils verschiedener Algorithmen bedarf, ist eine fundamentale Eigenschaft des Problems an sich; die Auswahl des Algorithmus ist rein prinzipiell an die Kombination der beteiligten Objekttypen gekoppelt, diese Kopplung ist ein notwendiger Umstand in jeder möglichen Lösung und kein unglücklicher Nebeneffekt eines bestimmten Lösungsansatzes. Double Dispatch liefert eine elegante Lösung für die Frage, wie man, gegeben eine beliebige Kombination von zwei Objekttypen, den passenden Algorithmus auswählen kann...
Diese Kopplung ist natürlich gegeben, aber das hat doch nichts damit zu tun, an welcher Stelle des Codes (örtlich gesehen) ich auf diese eingehe.
Angenommen ich möchte grundlegende Dinge an der Kollisonsabfrage ändern(1), neue Objekte hinzufügen(2) oder grundlegend verschiedene Kollisionsvarianten vorhalten(3), dann bedeutet das für die jeweilige Vorgehensweise:
Double Dispatch
- Ermitteln welche Objekte (Klassen -> Dateien) betroffen sind und Code in den jeweiligen Objekten ändern (1)
- Neue Kollisionsmethoden an einer Stelle für die neue Klasse erstellen und im Minimalfall nur den Rückruf in den anderen Klassen (quasi triple dispatch ;) ), ansonsten in allen beteiligten Klassen (2)
- Varianten in den Objekten vorhalten oder Varianten der Objekte vorhalten (3)
Kapselung mit Downcast
- Sämtlichen Code innerhalb des Kollisionsmanagers ändern (1)
- Neue Kollisionsmethoden an einer Stelle im Kollisionsmanager erstellen (2)
- Varianten der Kollisionsmanager vorhalten (3)
Auch ich finde Double-Dispatch elegant und habe es auch nicht grundlos anfangs implementiert. Aus rein praktischen Gründen empfand ich es aber zu verstreut im Code und wollte mich in allen drei oben genannten Fällen nur um eine Klasse/Datei kümmern. Ich finde Double-Dispatch mit Sicherheit nicht verkehrt, es ist eine persönliche Vorliebe gewesen, die gezeigte Alternative zu wählen. Was mir immer noch fehlt, ist ein wirklich praktischer Grund, was mir dabei früher oder später so sehr um die Ohren fliegt, dass es das Design schlecht macht.
dot hat geschrieben:Imo liegt da dein Problem. Sollten Emitter nicht viel eher Objekte in eine beliebige WorldDataStorage emittieren? Sollte es einer WorldDataStorage nicht absolut egal sein, was es alles für Arten von Emittern gibt oder vielleicht irgendwann mal geben wird? Abgesehen davon, würde ich mir die Frage stellen, ob es so eine gute Idee ist, einen solchen 'Emitter" wirklich ständig neue Objekte erzeugen und alte löschen zu lassen. Bei Partikelsystemen hat man in der Regel eine fixe, maximale Anzahl an Partikeln pro Emitter, die man einfach einmal alle erzeugen kann. Der Emitter führt dann nur noch das Updaten der Partikel durch. Meist will man in dem Moment, wo ein Partikel stirbt, direkt wieder ein neues emittieren. Anstatt das eine zu löschen und ein neues zu erzeugen, kann man einfach das bereits existierende resetten...
Ja, mein Fehler, siehe Edit in dem entsprechenden Post, wahrscheinlich hast Du ihn nicht mehr rechtzeitig gelesen. Die Emitter sind in einem eigenen "Storage" und emittieren in den WorldDataStorage. Partikel als solche sind in einem Ringpuffer und dieser hat eine fixe Größe. Es können aber auch physikalisch vollwertige Objekte erzeugt werden, die (undefiniert) länger leben, sie werden tatsächlich an den WorldDataStorage übergeben und sind dann als vollwertige Objekte in seiner Obhut. Und genau weil ich nicht ständig neue Objekte (im Speicher) erzeugen möchte, sollte der WorldDataStorage das Erzeugen der Objekte im Speicher übernehmen, wobei ich wieder beim Grund meines ursprünglichen Anliegens angekommen bin.
Edit: Copy-Paste-Fehler bei Punkt (3) der Kapselung-mit-Downcast-Liste korrigiert.