Ich habe gelesen, dass, wenn Ihr Programm wissen muss, um welche Klasse es sich bei einem Objekt handelt, dies normalerweise auf einen Konstruktionsfehler hinweist. Daher möchte ich wissen, was eine gute Vorgehensweise ist, um damit umzugehen. Ich implementiere eine Klassenform mit verschiedenen von ihr geerbten Unterklassen wie Kreis, Polygon oder Rechteck und habe verschiedene Algorithmen, um festzustellen, ob ein Kreis mit einem Polygon oder einem Rechteck kollidiert. Nehmen wir dann an, wir haben zwei Instanzen von Shape und möchten wissen, ob eine mit der anderen kollidiert. Bei dieser Methode muss ich ableiten, welcher Unterklassentyp das Objekt ist, mit dem ich kollidiere, um zu wissen, welchen Algorithmus ich aufrufen soll, aber dies ist ein schlechtes Design oder schlechte Praxis? So habe ich es gelöst.
abstract class Shape {
ShapeType getType();
bool collide(Shape other);
}
class Circle : Shape {
getType() { return Type.Circle; }
bool collide(Shape other) {
if(other.getType() == Type.Rect) {
collideCircleRect(this, (Rect) other);
} else if(other.getType() == Type.Polygon) {
collideCirclePolygon(this, (Polygon) other);
}
}
}
Dies ist ein schlechtes Designmuster? Wie könnte ich das lösen, ohne auf die Unterklassentypen schließen zu müssen?
quelle
Shape
Objekten. Ihre Logik hängt von den Interna anderer Objekte ab, es sei denn, Sie überprüfen die Kollision auf Grenzpunktebool collide(x, y)
(eine Teilmenge der Kontrollpunkte kann ein guter Kompromiss sein). Andernfalls müssen Sie den Typ irgendwie überprüfen. Wenn wirklich Abstraktionen erforderlich sind, sollte die Erstellung vonCollision
Typen (für Objekte im Bereich des aktuellen Schauspielers) der richtige Ansatz sein.Antworten:
Polymorphismus
Solange Sie
getType()
oder etwas Ähnliches verwenden, verwenden Sie keinen Polymorphismus.Ich verstehe das Gefühl, dass Sie wissen müssen, welchen Typ Sie haben. Aber jede Arbeit, die Sie machen möchten, während Sie wissen, dass dies wirklich in die Klasse gedrängt werden sollte. Dann sagst du einfach, wann es geht.
Dieses Prinzip heißt erzählen, nicht fragen . Wenn Sie dem folgen, können Sie Details wie Typ nicht verbreiten und eine Logik erstellen, die auf sie einwirkt. Dadurch wird eine Klasse auf den Kopf gestellt. Es ist besser, dieses Verhalten in der Klasse beizubehalten, damit es sich ändern kann, wenn sich die Klasse ändert.
Verkapselung
Sie können mir sagen, dass niemals andere Formen benötigt werden, aber ich glaube Ihnen nicht und Sie sollten es auch nicht.
Ein netter Effekt der folgenden Kapselung ist, dass es einfach ist, neue Typen hinzuzufügen, da sich ihre Details nicht auf den Code verteilen, in dem sie angezeigt werden,
if
und auf dieswitch
Logik. Der Code für einen neuen Typ sollte sich alle an einer Stelle befinden.Ein typunabhängiges Kollisionserkennungssystem
Lassen Sie mich Ihnen zeigen, wie ich ein Kollisionserkennungssystem entwerfen würde, das performant ist und mit jeder 2D-Form funktioniert, ohne sich um den Typ zu kümmern.
Angenommen, Sie sollten das zeichnen. Scheint einfach. Es sind alles Kreise. Es ist verlockend, eine Kreisklasse zu erstellen, die Kollisionen versteht. Das Problem ist, dass wir dadurch eine Denkrichtung durchlaufen, die auseinanderfällt, wenn wir 1000 Kreise benötigen.
Wir sollten nicht an Kreise denken. Wir sollten über Pixel nachdenken.
Was wäre, wenn ich Ihnen sagen würde, dass Sie denselben Code, mit dem Sie diese Typen zeichnen, erkennen können, wenn sie sich berühren oder auf welchen der Benutzer klickt?
Hier habe ich jeden Kreis mit einer einzigartigen Farbe gezeichnet (wenn Ihre Augen gut genug sind, um den schwarzen Umriss zu sehen, ignorieren Sie das einfach). Dies bedeutet, dass jedes Pixel in diesem versteckten Bild dem entspricht, was es gezeichnet hat. Eine Hashmap kümmert sich gut darum. Auf diese Weise können Sie tatsächlich Polymorphismus betreiben.
Dieses Bild müssen Sie dem Benutzer nie zeigen. Sie erstellen es mit demselben Code, der den ersten gezeichnet hat. Nur mit verschiedenen Farben.
Wenn der Benutzer auf einen Kreis klickt, weiß ich genau, welcher Kreis, weil nur ein Kreis diese Farbe hat.
Wenn ich einen Kreis über einen anderen zeichne, kann ich schnell jedes Pixel lesen, das ich überschreiben möchte, indem ich sie in einem Satz ablege. Wenn ich mit den Sollwerten für jeden Kreis fertig bin, mit dem er kollidiert ist, muss ich jeden nur noch einmal anrufen, um ihn über die Kollision zu informieren.
Ein neuer Typ: Rechtecke
Dies wurde alles mit Kreisen gemacht, aber ich frage Sie: Würde es mit Rechtecken anders funktionieren?
In das Erkennungssystem ist kein Kreiswissen gelangt. Radius, Umfang oder Mittelpunkt sind egal. Es kümmert sich um Pixel und Farbe.
Der einzige Teil dieses Kollisionssystems, der in die einzelnen Formen gedrückt werden muss, ist eine einzigartige Farbe. Ansonsten können die Formen nur daran denken, ihre Formen zu zeichnen. Darin sind sie sowieso gut.
Wenn Sie jetzt die Kollisionslogik schreiben, ist es Ihnen egal, welchen Subtyp Sie haben. Sie sagen, es soll kollidieren und es sagt Ihnen, was es unter der Form gefunden hat, die es vorgibt zu zeichnen. Typ muss nicht bekannt sein. Das bedeutet, dass Sie so viele Untertypen hinzufügen können, wie Sie möchten, ohne den Code in anderen Klassen aktualisieren zu müssen.
Implementierungsoptionen
Wirklich, es muss keine eindeutige Farbe sein. Dies können tatsächliche Objektreferenzen sein und eine Indirektionsebene speichern. Aber diese sehen in dieser Antwort nicht so gut aus.
Dies ist nur ein Implementierungsbeispiel. Es gibt sicherlich andere. Dies sollte zeigen, dass das gesamte System umso besser funktioniert, je näher Sie diese Formuntertypen an ihrer einzigen Verantwortung festhalten. Es gibt wahrscheinlich schnellere und weniger speicherintensive Lösungen, aber wenn sie mich dazu zwingen, das Wissen über die Subtypen zu verbreiten, würde ich es ablehnen, sie auch bei Leistungssteigerungen zu verwenden. Ich würde sie nicht benutzen, wenn ich sie nicht eindeutig brauchte.
Doppelversand
Bisher habe ich den Doppelversand völlig ignoriert . Ich habe das getan, weil ich konnte. Solange es der Kollisionslogik egal ist, welche beiden Typen kollidierten, brauchen Sie sie nicht. Wenn Sie es nicht brauchen, verwenden Sie es nicht. Wenn Sie glauben, dass Sie es brauchen könnten, verschieben Sie den Umgang damit so lange wie möglich. Diese Haltung nennt man YAGNI .
Wenn Sie entscheiden, dass Sie wirklich verschiedene Arten von Kollisionen benötigen, fragen Sie sich selbst, ob n-Form-Subtypen wirklich n 2 Arten von Kollisionen benötigen . Bisher habe ich sehr hart gearbeitet, um das Hinzufügen eines weiteren Formuntertyps zu vereinfachen. Ich möchte es nicht mit einer Doppelversand-Implementierung verderben , die Kreise dazu zwingt, zu wissen, dass Quadrate existieren.
Wie viele Arten von Kollisionen gibt es überhaupt? Ein wenig Spekulieren (eine gefährliche Sache) erfindet elastische Kollisionen (federnd), unelastisch (klebrig), energisch (explodieren) und destruktiv (schädlich). Es könnte mehr geben, aber wenn dies weniger als n 2 ist, können wir unsere Kollisionen nicht überdenken.
Das heißt, wenn mein Torpedo etwas trifft, das Schaden akzeptiert, muss er nicht wissen, dass er ein Raumschiff trifft. Es muss nur sagen: "Ha ha! Du hast 5 Schadenspunkte genommen."
Dinge, die Schaden verursachen, senden Schadensmeldungen an Dinge, die Schadensmeldungen annehmen. Auf diese Weise können Sie neue Formen hinzufügen, ohne die anderen Formen über die neue Form zu informieren. Sie verbreiten sich nur über neue Arten von Kollisionen.
Das Raumschiff kann zum Torp zurückschicken: "Ha ha! Du hast 100 Schadenspunkte genommen." sowie "Du steckst jetzt an meinem Rumpf fest". Und der Torp kann zurückschicken "Nun, ich bin fertig damit, mich zu vergessen".
Zu keinem Zeitpunkt weiß einer genau, was jeder ist. Sie wissen nur, wie sie über eine Kollisionsschnittstelle miteinander sprechen können.
Mit dem doppelten Versand können Sie die Dinge genauer steuern, aber möchten Sie das wirklich ?
Wenn Sie dies tun, denken Sie bitte zumindest darüber nach, einen doppelten Versand durch Abstraktionen darüber durchzuführen, welche Arten von Kollisionen eine Form akzeptiert, und nicht über die tatsächliche Formimplementierung. Kollisionsverhalten können Sie auch als Abhängigkeit einfügen und an diese Abhängigkeit delegieren.
Performance
Leistung ist immer kritisch. Das heißt aber nicht, dass es immer ein Problem ist. Testleistung. Spekulieren Sie nicht nur. Alles andere im Namen der Leistung zu opfern, führt normalerweise sowieso nicht zu Perforationscode.
quelle
Die Beschreibung des Problems klingt so, als ob Sie Multimethoden (auch als Mehrfachversand bezeichnet) verwenden sollten, in diesem speziellen Fall - Doppelversand . Die erste Antwort ging ausführlich auf den generischen Umgang mit kollidierenden Formen beim Raster-Rendering ein, aber ich glaube, OP wollte eine "Vektor" -Lösung, oder vielleicht wurde das gesamte Problem in Form von Formen neu formuliert, was ein klassisches Beispiel in OOP-Erklärungen ist.
Selbst der zitierte Wikipedia-Artikel verwendet dieselbe Kollisionsmetapher. Lassen Sie mich nur zitieren (Python verfügt nicht über integrierte Multimethoden wie einige andere Sprachen):
Die nächste Frage ist also, wie Sie Unterstützung für Multimethoden in Ihrer Programmiersprache erhalten.
quelle
Dieses Problem erfordert eine Neugestaltung auf zwei Ebenen.
Zunächst müssen Sie die Logik zum Erkennen der Kollision zwischen den Formen aus den Formen herausziehen. Auf diese Weise würden Sie OCP nicht jedes Mal verletzen, wenn Sie dem Modell eine neue Form hinzufügen müssen. Stellen Sie sich vor, Sie haben bereits Kreis, Quadrat und Rechteck definiert. Sie könnten es dann so machen:
Als Nächstes müssen Sie festlegen, dass die entsprechende Methode abhängig von der Form aufgerufen wird, die sie aufruft. Sie können dies mithilfe von Polymorphismus und Besuchermuster tun . Um dies zu erreichen, müssen wir das entsprechende Objektmodell haben. Erstens müssen alle Formen an derselben Schnittstelle haften:
Als nächstes müssen wir eine übergeordnete Besucherklasse haben:
Ich verwende hier eine Klasse anstelle der Schnittstelle, da jedes Besucherobjekt ein Attribut vom
ShapeCollisionDetector
Typ haben muss.Jede Implementierung der
IShape
Schnittstelle würde den entsprechenden Besucher instanziieren und die entsprechendeAccept
Methode des Objekts aufrufen, mit dem das aufrufende Objekt interagiert, wie folgt:Und bestimmte Besucher würden so aussehen:
Auf diese Weise müssen Sie die Formklassen nicht jedes Mal ändern, wenn Sie eine neue Form hinzufügen, und Sie müssen nicht nach dem Formtyp suchen, um die entsprechende Kollisionserkennungsmethode aufzurufen.
Ein Nachteil dieser Lösung besteht darin, dass Sie beim Hinzufügen einer neuen Form die ShapeVisitor-Klasse um die Methode für diese Form erweitern müssen (z. B.
VisitTriangle(Triangle triangle)
) und diese Methode folglich in allen anderen Besuchern implementieren müssen. Da dies jedoch eine Erweiterung ist, in dem Sinne, dass keine vorhandenen Methoden geändert werden, sondern nur neue hinzugefügt werden, verstößt dies nicht gegen OCP , und der Code-Overhead ist minimal. Durch die Verwendung der KlasseShapeCollisionDetector
vermeiden Sie außerdem die Verletzung von SRP und Code-Redundanz.quelle
Ihr Grundproblem besteht darin, dass in den meisten modernen OO-Programmiersprachen die Funktionsüberladung bei dynamischer Bindung nicht funktioniert (dh die Art der Funktionsargumente wird zur Kompilierungszeit bestimmt). Was Sie benötigen würden, ist ein virtueller Methodenaufruf, der für zwei Objekte und nicht nur für ein Objekt virtuell ist. Solche Methoden werden Multi-Methoden genannt . Es gibt jedoch Möglichkeiten, dieses Verhalten in Sprachen wie Java, C ++ usw. zu emulieren . Hier ist der doppelte Versand sehr praktisch.
Die Grundidee ist, dass Sie den Polymorphismus zweimal verwenden. Wenn zwei Formen kollidieren, können Sie die richtige Kollisionsmethode eines der Objekte durch Polymorphismus aufrufen und das andere Objekt des generischen Formtyps übergeben. In der aufgerufenen Methode wissen Sie dann, ob dieses Objekt ein Kreis, ein Rechteck oder was auch immer ist. Anschließend rufen Sie die Kollisionsmethode für das übergebene Formobjekt auf und übergeben es an dieses Objekt. Dieser zweite Aufruf findet dann durch Polymorphismus wieder den richtigen Objekttyp.
Ein großer Nachteil dieser Technik ist jedoch, dass jede Klasse in der Hierarchie über alle Geschwister Bescheid wissen muss. Dies stellt einen hohen Wartungsaufwand dar, wenn später eine neue Form hinzugefügt wird.
quelle
Vielleicht ist dies nicht der beste Weg, um dieses Problem anzugehen
Die mathematische Formkollision ist speziell für die Formkombinationen. Dies bedeutet, dass die Anzahl der Unterprogramme, die Sie benötigen, das Quadrat der Anzahl der Formen ist, die Ihr System unterstützt. Bei den Formkollisionen handelt es sich nicht um Operationen an Formen, sondern um Operationen, bei denen Formen als Parameter verwendet werden.
Überlastungsstrategie für Bediener
Wenn Sie das zugrunde liegende mathematische Problem nicht vereinfachen können, würde ich den Ansatz der Operatorüberladung empfehlen. Etwas wie:
Auf dem statischen Intializer würde ich mithilfe der Reflexion eine Karte der Methoden erstellen, um einen diynamischen Dispather für die generische Kollisionsmethode (Shape s1, Shape s2) zu implementieren. Der statische Intializer kann auch eine Logik haben, um fehlende Kollisionsfunktionen zu erkennen und zu melden, ohne die Klasse zu laden.
Dies ähnelt der Überladung des C ++ - Operators. In C ++ ist die Überladung von Operatoren sehr verwirrend, da Sie einen festen Satz von Symbolen haben, die Sie überladen können. Das Konzept ist jedoch sehr interessant und kann mit statischen Funktionen repliziert werden.
Der Grund, warum ich diesen Ansatz verwenden würde, ist, dass Kollision keine Operation über einem Objekt ist. Eine Kollision ist eine externe Operation, die eine Beziehung zu zwei beliebigen Objekten besagt. Außerdem kann der statische Initialisierer prüfen, ob eine Kollisionsfunktion fehlt.
Vereinfachen Sie Ihr mathematisches Problem, wenn möglich
Wie bereits erwähnt, ist die Anzahl der Kollisionsfunktionen das Quadrat der Anzahl der Formtypen. Dies bedeutet, dass Sie in einem System mit nur 20 Formen 400 Routinen benötigen, mit 21 Formen 441 und so weiter. Dies ist nicht einfach zu erweitern.
Aber Sie können Ihre Mathematik vereinfachen . Anstatt die Kollisionsfunktion zu erweitern, können Sie jede Form rastern oder triangulieren. Auf diese Weise muss die Kollisionsmaschine nicht erweiterbar sein. Kollision, Entfernung, Schnittpunkt, Zusammenführen und verschiedene andere Funktionen sind universell.
Triangulieren
Haben Sie bemerkt, dass die meisten 3D-Pakete und Spiele alles triangulieren? Das ist eine der Formen der Vereinfachung der Mathematik. Dies gilt auch für 2D-Formen. Polys können trianguliert werden. Kreise und Splines können an Poligons angenähert werden.
Wieder ... haben Sie eine einzige Kollisionsfunktion. Ihre Klasse wird dann:
Und Ihre Operationen:
Einfacher ist es nicht?
Rasterisieren
Sie können Ihre Form rastern, um eine einzige Kollisionsfunktion zu erhalten.
Die Rasterung scheint eine radikale Lösung zu sein, kann jedoch erschwinglich und schnell sein, je nachdem, wie genau Ihre Formkollisionen sein müssen. Wenn sie nicht präzise sein müssen (wie in einem Spiel), haben Sie möglicherweise Bitmaps mit niedriger Auflösung. Die meisten Anwendungen erfordern keine absolute Genauigkeit in der Mathematik.
Annäherungen können gut genug sein. Der ANTON-Supercomputer für die Biologie-Simulation ist ein Beispiel. In seiner Mathematik werden viele schwer zu berechnende Quanteneffekte verworfen, und die bisher durchgeführten Simulationen stimmen mit den in der realen Welt durchgeführten Experimenten überein. Die in Game-Engines und Rendering-Paketen verwendeten PBR-Computergrafikmodelle vereinfachen die Computerleistung, die zum Rendern der einzelnen Frames erforderlich ist. Ist nicht wirklich physikalisch genau, aber nah genug, um mit bloßem Auge zu überzeugen.
quelle