Design-Frage: Zugriff verwaltender Klasse auf verwaltetes Objekt
-
Nexus schrieb:
Diese Aufgaben hatte ich zuerst in
Objectselber geregelt, aber es erschien mir sinnvoll, sie auszulagern.Vielleicht ist es doch nicht so sinnvoll, sie auszulagern. Eine Verantwortlichkeit sollte bei dem Objekt liegen, dass die Informationen besitzt, um sie auszufüllen. Das läuft auch unter dem Titel "Tell, don't ask".
Wenn du Informationen abfragst, darauf basierend Entscheidungen triffst und das Objekt daraufhin instruierst, seinen Zustand entsprechend zu verändern, wandert Nach und nach immer mehr Verhalten von den Objekten zu irgendwelchen "Managern". Damit betreibst du im Grunde prozedurale Programmierung.
-
Vielen Dank für eure Antworten.
@ Prog-Child
An die Referenz-Rückgabe habe ich gedacht, weil ich so eine einheitliche Schnittstelle hätte (Zugriff nur über Funktionen) und zudem bei Bedarf noch andere Anweisungen in die Methode schreiben könnte.Naja, die Abhängigkeiten sind relativ kompliziert. Ich habe Tiles, den Spieler, Gegner, Gegenstände, abgefeuerte Waffen und Lifte (wovon einiges noch nicht implementiert ist). Da gibt es relativ viele Kombinationen, weswegen ich auch eine eigene Klasse dafür zuständig gemacht habe. Ausserdem habe ich eine eigene Art der Kollisionsabfrage, wahrscheinlich komplexer als nur BoundingBoxes. Daher könnte ich auch die Schnittstelle nicht eng halten.
@ Bashar
Ja, das ging mir auch durch den Kopf. Das Problem ist wie gesagt, dass nicht nur dasObjectan sich Kollision benötigt. Ich wollte lieber die einzelnen Spielelemente (Tiles, Spieler, ...) voneinander unabhängig gestalten und einen gemeinsamen VerwalterPhysicManagerbestimmen.Ist auch interessant, letztens wurde mir empfohlen, die Spielelemente nur als Figuren zu belassen, die gar nichts selber tun.

Momentan habe ich etwas dazwischen, wobei ich eher zur zentralen Verwaltung tendiere.
-
Hmm. Wenn es wirklich diese Richtung sein muss, kann man das auch ein wenig anderst lösen, ohne die Kapselung zu vernichten.
Mal davon ausgegangen, dass man (mehr oder weniger) immer alle Daten holt und wieder zurückschreiben möchte, kann man auch ein struct nehmen, wo man die Daten reinspeichert und dann dieses hin und herschiebt.struct data { float bla; float das; float dasandere; ... }; class foo { data d_; public: const data & getData () const { return d_; } void setData ( data& d ) { //überprüfe/verändere alles, was in d ankommt d_ = d; } };Aber grundsätzlich muss ich Bashar zustimmen, dass das Objekt sich kümmern sollte.
-
structfinde ich von daher nicht gut, da dann alle anderen Klassen auch Vollzugriff haben. Zudem steht das Ganze noch in einer Vererbungshierarchie.
-
Nexus schrieb:
Ja, das ging mir auch durch den Kopf. Das Problem ist wie gesagt, dass nicht nur das
Objectan sich Kollision benötigt. Ich wollte lieber die einzelnen Spielelemente (Tiles, Spieler, ...) voneinander unabhängig gestalten und einen gemeinsamen VerwalterPhysicManagerbestimmen.Aber, wenn du konsequent bist, so ist der PhysicManager von jedem deiner Objekte abhängig.
Wenn du Kollision kapseln willst, dann erstelle die Objekte, die miteinander kollidieren sollen. Die machst du miteinander bekannt. Du musst ja nichts weiter in die Klasse packen, also keine weitere Funktionalität. Dann kann jedes deiner Objekte ein Objekt zurückgeben, dass nur die Kollision charakterisiert... z.B.
-
ProgChild schrieb:
Aber, wenn du konsequent bist, so ist der PhysicManager von jedem deiner Objekte abhängig.
Ja, das ist so. Aber irgendwo muss man ja auch eine Abhängigkeit haben...
ProgChild schrieb:
Wenn du Kollision kapseln willst, dann erstelle die Objekte, die miteinander kollidieren sollen. Die machst du miteinander bekannt. Du musst ja nichts weiter in die Klasse packen, also keine weitere Funktionalität.
Eigentlich wollte ich das vermeiden. Also dass ein Gegner über alle anderen Objekte Bescheid weiss. Dann muss er nämlich auch über Spieler, Waffen, Tiles und Lifte Bescheid wissen. Der Spieler muss dann wiederum über Gegner, Waffen, Gegenstände, Lifte und Tiles Bescheid wissen. Das führt zu einer asymmetrischen Informationsverlagerung und möglicherweise sogar zu Codeduplikation. Zusätzlich kann man hier sehr schnell die Übersicht verlieren. Deshalb auch der Ansatz mit der zentralen Kollisionsverwaltung.
-
@ Progchild:
Ist zwar Off-Topic, aber du hast ein Schreibfehler in deinem Programm.
Dort steht etwas von "Receave". Es heißt aber "receive"
Siehe:
http://www.progchild.de/software/netwalker/images/shot_01.jpg
http://www.progchild.de/software/netwalker/images/shot_02.jpg
-
Blubbb schrieb:
Ist zwar Off-Topic, aber du hast ein Schreibfehler in deinem Programm.
Dort steht etwas von "Receave". Es heißt aber "receive"
Ich weiß. Ich hab das auch schon seit Jahren geändert. Nur erstelle ich für sowas kein neues Build. Es liegt hier also weiterhin korrigiert auf meiner Festplatte. Wenn es dich stört, kannst du dir ja gerne den Quelltext runter laden und es ändern.
Btw. es heißt einen Schreibfehler nicht ein.
-
Darf ich fragen, wieso du direkt auf den Variablen des Modells rumrechnen möchtest? Das leuchtet mir nicht so ein.
Schon nur die Tatsache, falls in den Berechnungen ein Fehler passiert, der Fehler in den temporären Variablen haften bleibt und nicht im Objekt selbst, erscheint mir von Vorteil
Grundsätzlich kenne ich es so, dass man den Status eines Modells liest und kopiert, den Status neu berechnet und ihn wieder ins Modell zurückschreibt. Dem zuständigen Controller wird entweder automatisch oder manuell mitgeteilt, dass sich sein Objekt oder eines seiner Objekte verändert hat.
Falls du oft mehrere Daten zugleich brauchst, kannst du auch erweiterte Getter schreiben:
class Foo { private: int m_a; int m_b; int m_c; public: void get_abc(int& a, int& b, int& c) { a = m_a; b = m_b; c = m_c; } };Man könnte es sogar mit Zeiger erledigen, was einem die Möglichkeit gibt, nur einzelne Werte zu lesen:
void get_abc(int* a, int* b, int* c) { if(a) { *a = m_a; } if(b) { *b = m_b; } if(c) { *c = m_c; } }Gleiches gilt dann natürlich auch für den Setter. Es muss ja schliesslich nicht immer nur eine Variable gesetzt werden über einen Setter

Die Frage sei aber auch in den Raum gestellt, ob du richtig gekapselt hast

Grundsätzlich gefällt mir schon nur der Name von PhysicManager nicht. Erscheint mir irgendwie zu zentral.
Wenn ich nur zum Beispiel an eines meiner aktuelle Projekte denke, welches zwar derzeit etwas nebenher lahmt, da andere Projekte eine höhere Priorität haben, so habe ich dort folgende Klassen:
AreaMapping -> Aufteilung der Spielfläche in Sektionen.
CollisionDetection -> Die Objekte in gleichen Sektionen werden untereinander geprüft.
CollisionHandler -> Behandlen einer allfälligen Kollision.
GravitySystem -> Gravitation auf die Objekte anwenden.
usw.Viele davon sind teilweise nur Basisklassen, oft sogar abstrakt. Ableitungen geben dann spezielle Eigenschaften.
Ohne Getter und Setter in den Modellen der Objekte, gäbe es da gar keine Möglichkeit. Ich kann ja nicht all diese Klassen zufrienddeklarieren
Nexus schrieb:
Naheliegend sind Getter und Setter, allerdings müsste ich relativ viele Methoden schreiben.
Diese Aussage verwundert mich allerdings auch noch ein wenig. Von wievielen Methoden reden wir hier? Und suchst du eine andere Lösung, nur um deine Schreibfaulheit zu umgehen?

Wieviele Eigenschaften stecken bei dir denn in einem Object? Womöglich zuviele?Grüssli
-
Dravere schrieb:
Darf ich fragen, wieso du direkt auf den Variablen des Modells rumrechnen möchtest? Das leuchtet mir nicht so ein.
Das ist nur gerade wie es aktuell aussieht, ich frage ja nach einer besseren Lösung.
Dravere schrieb:
Schon nur die Tatsache, falls in den Berechnungen ein Fehler passiert, der Fehler in den temporären Variablen haften bleibt und nicht im Objekt selbst, erscheint mir von Vorteil

In den Berechnungen passieren aber keine Fehler. Falls es doch der Fall sein sollte (Bugs gibts immer, ich hab noch nicht gross getestet), werde ich die kaum zur Laufzeit abfragen, sondern gleich im Quellcode beheben.

Dravere schrieb:
Die Frage sei aber auch in den Raum gestellt, ob du richtig gekapselt hast

Grundsätzlich gefällt mir schon nur der Name von PhysicManager nicht. Erscheint mir irgendwie zu zentral.
Wenn ich nur zum Beispiel an eines meiner aktuelle Projekte denke, welches zwar derzeit etwas nebenher lahmt, da andere Projekte eine höhere Priorität haben, so habe ich dort folgende Klassen:
AreaMapping -> Aufteilung der Spielfläche in Sektionen.
CollisionDetection -> Die Objekte in gleichen Sektionen werden untereinander geprüft.
CollisionHandler -> Behandlen einer allfälligen Kollision.
GravitySystem -> Gravitation auf die Objekte anwenden.
usw.Momentan besteht die Physik eigentlich nicht aus viel mehr als Kollisionsabfrage/-reaktion und Gravitation/Bewegung. Diese beiden Aufgaben möglicherweise noch in einzelne Klassen aufspalten, aber das lässt sich auch leicht in Zukunft durchführen. Ich muss mir sowieso noch überlegen, wie ich das Ganze mache, wenn noch mehr Spielelemente dazu kommen. Kollisionsabfrage und -reaktion sind momentan aber so eng verbunden, dass ich die unmöglich auftrennen kann. Dazu sehe ich aber auch keinen Anlass.
Dravere schrieb:
Diese Aussage verwundert mich allerdings auch noch ein wenig. Von wievielen Methoden reden wir hier? Und suchst du eine andere Lösung, nur um deine Schreibfaulheit zu umgehen?

Ich hab bereits beim Schreiben gedacht, dass man dies falsch interpretieren könnte. Die Faulheit ist nicht das Problem, sondern dass nachher zu viele Methoden in der Klasse stecken. Wobei das eigentlich nicht sehr schlimm ist.
Dravere schrieb:
Wieviele Eigenschaften stecken bei dir denn in einem Object? Womöglich zuviele?
Aktuell sind es nur 6 Membervariablen, das sollte eigentlich legitim sein. Das Problem ist, dass drei davon zweidimensionale Vektoren sind, und man bei denen teilweise nur eine Komponente ändern muss. Dann müsste ich entweder immer hässliche Konstrukte bauen:
Object.SetVector(NewX, Object.GetVector().y);Oder ich überleg mir deine Variante mit den optionalen Angaben. Das tönt wirklich interessant.
-
Nexus schrieb:
In den Berechnungen passieren aber keine Fehler. Falls es doch der Fall sein sollte (Bugs gibts immer, ich hab noch nicht gross getestet), werde ich die kaum zur Laufzeit abfragen, sondern gleich im Quellcode beheben.

Ich meinte nicht nur solche Fehler, sondern auch logische Spielfehler.
Es ist manchmal deutlich einfacher das Resultat auf Gültigkeit zu prüfen, als im voraus probieren die Gültigkeit des Resultates abzuschätzen. Wenn du nun direkt auf dem Modell rechnest, dann musst du am Ende die Sache womöglich zurücksetzen. Bei meinem Fall verwerfe ich einfach das Resultat.
Aber es kommt natürlich darauf an, wie deine Implementation aussieht. Das sind alles sehr theoretische und allgemein gehaltene Konstrukte, welche ich da aufführe
Nexus schrieb:
Kollisionsabfrage und -reaktion sind momentan aber so eng verbunden, dass ich die unmöglich auftrennen kann.
Ich kann da natürlich auch nur theoretisch antworten. Aber die Idee dahinter ist, dass die Kollision und Reaktion unterschiedliche Dinge sind. Zum einen hat man Kollisionen, welche womöglich auf unterschiedlichen Ebenen stattfinden, zum anderen hat man Reaktionen, welche je nach Umgebung und anderen Einflüssen unterschiedlich sein können (abgesehen vom Material der kollidierenden Objekte).
Dem Kollisionsabfrage-Objekt kann man nun verschiedene Kollisionsreaktion-Objekte übergeben. Bei der Kollision wird dann das entsprechend korrekte Kollisionsreaktion-Objekt aufgerufen.
Als theoretisches Beispiel:
Sagen wir du hast unterschiedliche Levels und in einem Level sollen Kollisionen deutlich schwächere Reaktionen haben, als in einem anderen. Du speicherst einfach ein entsprechendes Kollisionsreaktion-Objekt ab. Sobald das Level betreten wird, wechselst du das Kollisionsreaktion-Objekt im Kollionsionsabfrage-Objekt aus. Das kann womöglich nur ein Zeiger sein.
Das ist eine äusserst kurze Anweisung und schon läuft alles, wie man es möchte (sofern keine Programmierfehler vorhanden sind ;)). Am Ende des Levels kann man mit der gleichen kurzen Anweisung, die Sache wieder zurück tauschen.Nexus schrieb:
Aktuell sind es nur 6 Membervariablen, das sollte eigentlich legitim sein. Das Problem ist, dass drei davon zweidimensionale Vektoren sind, und man bei denen teilweise nur eine Komponente ändern muss. Dann müsste ich entweder immer hässliche Konstrukte bauen:
Object.SetVector(NewX, Object.GetVector().y);Oder ich überleg mir deine Variante mit den optionalen Angaben. Das tönt wirklich interessant.
Oder du überlegst dir zum Beispiel sowas:
template<typename ValueT, typename ObjectT> void set_vector_x(ObjectT& object, ValueT const& value) { object.set_vector(value, object.get_vector().y); } template<typename ValueT, typename ObjectT> void set_vector_y(ObjectT& object, ValueT const& value) { object.set_vector(object.get_vector().x, value); }Man könnte auch in der Klasse selbst eine
set_xundset_yMethode einbauen. Mit den Templates, und wenn man ein einheitliches Interface hat, kann man sich natürlich Schreibarbeit sparen.
Du könntest aber natürlich auch immer den ganzen Vektor kopieren und dann auf der Variable des kopierten Vektors rumrechnen. Am Ende einfach den ganzen Vektor zurückschreiben. Würde mich nicht überraschen, wenn der Compiler erkennt, dass die andere Variable nicht berührt wird und es daher wegoptimiert. Aber naja, bereits der Gedanke an Optimierung, vor allem in so einem Fall, stösst in die Richtung von PMO
Grüssli
-
Danke für deine Antworten.
Dravere schrieb:
Ich kann da natürlich auch nur theoretisch antworten. Aber die Idee dahinter ist, dass die Kollision und Reaktion unterschiedliche Dinge sind. Zum einen hat man Kollisionen, welche womöglich auf unterschiedlichen Ebenen stattfinden, zum anderen hat man Reaktionen, welche je nach Umgebung und anderen Einflüssen unterschiedlich sein können (abgesehen vom Material der kollidierenden Objekte).
Ja, grundsätzlich hast du hier schon Recht. Allerdings gibt es bei zwei Dinge, die dagegen sprechen: Erstens ist die Kollision überall gleich, also allgemeingültig (spricht zwar nicht dagegen, aber die Abstraktion bringt nicht sehr viel). Das zweite, und viel kritischere besteht in der Implementierung. Diese ist nämlich ziemliches Frickelwerk, da eine eigene Kollisionsbehandlung habe und sehr viele Spezialfälle abdecken muss. Vieles ist läuft nach dem Schema
if (Kollision) { Behandlung; }. Und ich bin froh, dass das Ganze nun läuft, da habe ich keine Lust, das alles umzukrempeln (kannst du sicher nachvollziehen).

Sowieso stelle ich es mir schwieriger vor, wenn die Fehler nicht gleich lokal behandelt werden können. Dann müssen womöglich noch diverse Statusflags übergeben werden, was die Sache auch nicht gerade vereinfacht.Die Sache mit den Templates ist natürlich auch eine Möglichkeit. Aber momentan sollte es nicht zu schlimm sein wegen der Memberfunktionen. Ich habe bei mir auch nicht mehrere Klassen mit gleicher Schnittstelle, die als Typ-Argument
ObjectTdienen könnten.Also würdest du die Variablen nicht direkt ändern, sondern zuerst kopieren und dann wieder setzen:
void PhysicManager::HandleCollision(Object& CurObj) { float vx = CurObj.GetVelocity().x; float vy = CurObj.GetVelocity().y; // hier eigentlicher Algorithmus CurObj.SetVelocity(Vector2f(vx, vy)); }Oder ich biete spezifischere Funktionen wie
void SetVelocity(float, float)an...
-
Nexus schrieb:
Diese ist nämlich ziemliches Frickelwerk, da eine eigene Kollisionsbehandlung habe und sehr viele Spezialfälle abdecken muss.
Das ist aber gar nicht gut

Aber naja ...Nexus schrieb:
Also würdest du die Variablen nicht direkt ändern, sondern zuerst kopieren und dann wieder setzen:
Oder ich biete spezifischere Funktionen wie
void SetVelocity(float, float)an...Es kommt natürlich teilweise darauf an, was ich mache, aber ich probiere mich an so ein Prinzip zu halten.
Abgesehen vom praktischen Nutzen, welchen ich dahinter sehe, empfinde ich auch diese Schreibweise als lesbarer. Es entspricht dem Gedankengang einer Funktionsweise.Gedankengang bei deiner Version:
-> Attribute als Referenz gelesen, ok, die Geschwindigkeit ist also in vx und vy drin.
-> Komplizierte Berechnung. Wie geht das, ach so. Ja ... ok ... hmmm ... gut ... (in der Zwischenzeit vernichtet das Gehirn die Erinnerung daran, dass vx und vy Referenzen sind, da es die Ressourcen für anderes braucht) ... ok, interessant, gut ... das funktioniert ... verstanden.
-> Funktionsende? Und das Ergebnis? Wo, wie, was?
-> Raufscrollen, rückwärts lesen des Codes, bzw. überfliegen. Irgendwas übersehen?
-> Ah, es waren Referenzen ...
-> Je nachdem nochmals runterscrollen, um sich dies nochmals klar zu machen, was das nun genau bedeutet. Die Zuweisung findet im Algorithmus statt.Gedankengang bei meiner Version:
-> Attribute gelesen, ok, die Geschwindigkeit ist also in vx und vy drin.
-> Komplizierte Berechnung (ich wiederhole nicht nochmals all diese kleinen Wörter :))
-> Setzen der Attribute, gut.
-> Funktionsende, verstanden.Grüssli
-
Dravere schrieb:
Das ist aber gar nicht gut

Aber naja ...Hätte mich interessiert, ob du das sauber hingekriegt hättest. Und denk dran, es ist nicht umso besser, je mehr Klassen und Dateien das Ganze benötigt. :p
Ok, das mit dem Gedankengang ist auch sehr subjektiv. Ich weiss ja, dass die Werte als Referenzen genommen werden, beim Anschauen hab ich das noch nicht vergessen.

Aber mit der grundsätzlichen Funktionsweise stimmt es schon. Ausserdem ist dann die Schnittstelle von
Objectnicht hässlich (entweder direkter Zugriff auf Member oder Referenzrückgabe). Wahrscheinlich werde ich es mit herkömmlichen Gettern und Settern machen.
-
Nexus schrieb:
Hätte mich interessiert, ob du das sauber hingekriegt hättest. Und denk dran, es ist nicht umso besser, je mehr Klassen und Dateien das Ganze benötigt. :p

In meiner Theorie sicher *ist schliesslich ein grosser Theoretiker* :pNexus schrieb:
Ok, das mit dem Gedankengang ist auch sehr subjektiv. Ich weiss ja, dass die Werte als Referenzen genommen werden, beim Anschauen hab ich das noch nicht vergessen.

Du weisst es jetzt, aber was ist in einem Jahr? Oder wenn jemand anderes den Code anschauen soll.
Ich kann Spaghetti Code schreiben, wenn ich mich gerade mit dem Code beschäftige, dann verstehe ich auch alle gotos. Aber wenn ich mich nur einen Monat mit etwas anderem beschäftige und dann zurückkehre, dann habe ich unglaubliche Mühe, mich wieder einzuarbeiten
Aber es hat natürlich schon auch eine gewisse Subjektivität drin. Es geht schliesslich in Richtung von m_ oder nicht m_.

Grüssli
-
Also ich bevorzuge auch die Version, dass ich zuerst lese, bearbeite und dann zurückschreibe. Dann kann ich auch kürzere, lokale Variablen nehmen.
-
Dravere schrieb:
Du weisst es jetzt, aber was ist in einem Jahr? Oder wenn jemand anderes den Code anschauen soll.
Nein, das würde garantiert kein Problem darstellen. Die ersten beiden Anweisungen in der Funktion sind die Referenzinitialisierungen. Wenn man die übersieht (gerade wenn da noch ein Kommentar steht)...
So, ich habe es nun mit Gettern und Settern gemacht, aber aus dem Grund, dass zuerst gelesen wird, dann bearbeitet und anschliessend wieder geschrieben (wie drakon sagte). Allerdings ist das immer noch nicht ganz konsequent, da zwischendurch auch geschrieben wird (über Setter). Aber wenn man das wirklich durchziehen wollte, müsste man das Objekt kopieren.
Momentan sind es zwar etwa 25 Memberfunktionen in der
Object-Klasse, aber das finde ich nicht so tragisch. Viele wären eigentlich nicht direkt nötig, da sie andere aufrufen und zur vereinfachten und intuitiveren Nutzung der Klasse beitragen. Eventuell könnte ich mir überlegen, diese als freie Funktionen zu implementieren, aber momentan werde ich es wahrscheinlich so lassen...