[gelöst] Interface-Kapselung

Design Patterns, Erklärungen zu Algorithmen, Optimierung, Softwarearchitektur
Forumsregeln
Wenn das Problem mit einer Programmiersprache direkt zusammenhängt, bitte HIER posten.
Antworten
smurfer
Establishment
Beiträge: 198
Registriert: 25.02.2002, 14:55

[gelöst] Interface-Kapselung

Beitrag von smurfer »

Hallo allerseits,

ich möchte direkt loslegen und werde das Ganze mal auf ein einfaches Beispiel runterbrechen:

Angenommen ich habe ein Interface Shape und davon abgeleitet Klassen wie Polygon, Circle, Rectangle,...

Code: Alles auswählen

Shape
     |--Polygon
     |--Circle
     |--Rectangle
Die Instanzen müssen nun im Programm gepuffert werden (ich benötige den vorherigen Zeitschritt). Dabei hatte ich zunächst zwei "Listen" (std::list, std::vector oder was auch immer) mit Pointern auf Shapes. Allerdings wollte ich das double buffering vor dem Nutzer verbergen, daher gibt es ein Interface DoubleBufferedShape.

Zugriffe auf Methoden müssen immer sowohl für das Shape als auch für seinen Puffer erfolgen, da kein Aktualisieren des Puffers durch Kopieren erfolgt, sondern lediglich die Zeiger getauscht werden.

Die Shapes an sich sollen unverändert bleiben (zwischenzeitlich hatten sie jeweils einen Pointer auf ihren Puffer -- unschön), sämtliche zusätzliche Funktionalität soll von außen, vom DoubleBufferedShape kommen. Das ließe sich zunächst über (rein) virtuelle Methoden setAttribute machen, z.B.:

Code: Alles auswählen

DoubleBufferedShapeDerived::setAttribute(Attribute Attr)
{
  m_Shape->setAttribute(Attr);
  m_Buffer->setAttribute(Attr);
}
Allerdings besitzen nicht alle Shapes die gleichen Attribute, so hat Circle z.B. die Methode setRadius, die bei den anderen nicht vorhanden ist. Sie leer zu implementieren ist unschön. Natürlich könnte ich vom Interface DoubleBufferedShape wieder alle entsprechenden Shapes ableiten, also DoubleBufferedCircle beispielsweise, diesen Aufwand möchte ich mir eigentlich sparen. Ich bin mir nicht sicher, wie ich das ganze nun handhaben soll, am liebsten wäre mir etwas wie:

Code: Alles auswählen

DoubleBufferedShape.registerMethod("Circle::setRadius"); // Die Doppelpunktsyntax ist hier nur sinngemäß
zu Beginn und bei Aufruf dann

Code: Alles auswählen

DoubleBufferedShape.set("Radius", 5); // Syntax wiederum nur sinngemäß/repräsentativ
wobei intern für Shape und Buffer die entsprechende Methode aufgerufen wird. Nur bin ich mir nicht sicher, wie ich die Abbildung von Methodenparameter zu Methodenaufruf hinbekomme. Über eine Map beispielsweise mit Funktionspointern erreiche ich nur eine Methode, nicht beide (also auch die des Puffers). Alle möglichen Methoden "vorrätig" zu haben und über switch-case-Anweisungen zu gehen, halte ich auch für unschön.

Ich hoffe das Problem ist einigermaßen klar geworden, vielleicht ist das grundsätzliche Design, ein Interface in dieser Form zu kapseln schon wenig elegant?! Die zu setzenden Attribute sind bekannt, werden die Methoden nicht schon rein virtuell vorgehalten entsteht intern natürlich ein nicht ganz so hübsches

Code: Alles auswählen

static_cast<Circle*>(pShape)->setRadius(fRad);
Viele Grüße, smurfer

Edit: Lösung:

DoubleBufferedShape führt über getFront() und getBack() beispielsweise die Pointer zu den Shapes nach außen, dort können sie nach Belieben modifiziert werden. Das war schon vorher der Fall, allerdings war die Information dann nicht im jeweiligen Puffer vorhanden. Für den Fall (der Normalfall, wenn man nicht weiß, was man tut), dass sämtliche Informationen zum Puffer kopiert werden sollen, wird getShape() aufgerufen. Diese Methode liefert auch einen Zeiger auf das aktuelle Shape. Allerdings geschieht dies über die lokale Instanz einer Helferklasse, bei deren Destruktoraufruf die Information zum Puffer kopiert wird.
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [gelöst] Interface-Kapselung

Beitrag von CodingCat »

Warum gibt es den Zustand überhaupt zweifach? Wenn ohnehin alle Attribute in den Front-Buffer geschrieben werden, ist ein zusätzliches Speichern der Attribute im Shape-Objekt doch vollkommen unnötig? Inwiefern tauschst du "einfach" die Zeiger? Gehst du durch jedes Shape und ersetzt den Buffer-Zeiger? Dir ist klar, dass nach einem Tausch dein neuer Front-Buffer (gerade noch Back-Buffer) total veraltet ist, unabhängig davon, ob du den Zustand im Shape-Objekt doppelst; und dass diese Zustandsdivergenz nur durch eine Kopie aller im letzten Zeitschritt veränderten Shapes korrigiert werden kann?

Nebenbei: Wenn Shapes grundsätzlich double-buffered sind, warum dann diese seltsame Abstraktion mit DoubleBufferdShape-Wrapper?
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
smurfer
Establishment
Beiträge: 198
Registriert: 25.02.2002, 14:55

Re: [gelöst] Interface-Kapselung

Beitrag von smurfer »

Hallo CodingCat,

ich bin mir nicht ganz sicher, ob ich Deine Fragen richtig deute, ich versuche es mal zu beantworten.

Die Shapes sind nicht prinzipiell double-buffered, daher haben sie auch alle ihren eigenen Zustand. Ich fand es eigentlich ganz schön, ein Objekt, wie z.B. ein Shape, in seiner Form so zu belassen wie es ist und es durch eine einfache Kapselung puffern zu können. Vielleicht sind double-buffering und front-/back-buffer auch nicht die richtigen Begriffe, es handelt sich um physikalische Entitäten.

Das DoubleBufferedShape liefert einen Zeiger auf das aktuelle, gültige Shape_1 des (physikalischen) Frames. Der Aufruf einer update-Methode setzt den Zeiger auf Shape_2. In diesem Moment ist Shape_1 gültig für den vorherigen Zeitschritt, Shape_2 als aktuelles Shape ungültig. Nun erfolgt die Aktualisierung von Shape_2: Die ungültigen, veralteten Daten werden bei der physikalischen Integration direkt überschrieben. Das bedeutet also:

Physikframe t (Kollision, Zugriff durch Grafikengine etc) -> update (Zeiger tauschen) -> Integration t+1
statt
Physikframe t (Kollision, Zugriff durch Grafikengine etc) -> update (kopieren) -> Integration t+1

Einzig die Attribute, die nicht überschrieben (jedoch von extern geändert) werden, sind ungültig. Diese werden aber auch -- wie setRadius -- nur sporadisch aufgerufen und sollen durch das "seltsame" ;) Konstrukt abgefangen werden.
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [gelöst] Interface-Kapselung

Beitrag von CodingCat »

smurfer hat geschrieben:Das DoubleBufferedShape liefert einen Zeiger auf das aktuelle, gültige Shape_1 des (physikalischen) Frames. Der Aufruf einer update-Methode setzt den Zeiger auf Shape_2.
Okay, es gibt den aktuellen Zustand also nicht zweifach, sondern es gibt jedes Shape zweifach (ein aktuelles und ein altes), wobei jeweils eine zusammengehörige Shape-Sammlung einen "Buffer" darstellt? (m_shape und m_buffer sind dann irreführend und sollten eigentlich m_front und m_back heißen?)
smurfer hat geschrieben:In diesem Moment ist Shape_1 gültig für den vorherigen Zeitschritt, Shape_2 als aktuelles Shape ungültig. Nun erfolgt die Aktualisierung von Shape_2: Die ungültigen, veralteten Daten werden bei der physikalischen Integration direkt überschrieben.
[...]
Einzig die Attribute, die nicht überschrieben werden, sind ungültig. Diese werden aber auch -- wie setRadius -- nur sporadisch aufgerufen und sollen durch das "seltsame" ;) Konstrukt abgefangen werden.
Verstehe ich das richtig, dass du in diesem Fall alle Konfigurationsattribute (Radius etc.) bei Veränderung einfach blockweise in Front UND Back aktualisierst?

Mir ist noch immer nicht klar, was du mit dieser Pufferung schlussendlich bezweckst. Physik war jetzt immerhin ein Hinweis; soll die Integration multi-threaded auf dem Back Buffer laufen? Dann hast du aber noch immer ein Problem mit den Konfigurationsattributen: Zwar werden diese nun in beiden Buffers aktualisiert, dies kann jedoch zu beliebiger Zeit (in der Integration!) geschehen, wo du ganz sicher keine unsynchronisierte Veränderung des Radius o.ä. willst. Willst du hingegen vorerst kein Multithreading, so stellt sich die Frage, warum du Konfigurationsattribute wie den Radius überhaupt doppelt pufferst. Für eine stabile Simulation auf wohldefinierten Zeitschritten reicht dann die Pufferung der simulierten Attribute vollkommen aus; die Pufferung aller anderen Attribute macht dein Programm nur unnötig kompliziert.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
smurfer
Establishment
Beiträge: 198
Registriert: 25.02.2002, 14:55

Re: [gelöst] Interface-Kapselung

Beitrag von smurfer »

Okay, es gibt den aktuellen Zustand also nicht zweifach, sondern es gibt jedes Shape zweifach (ein aktuelles und ein altes), wobei jeweils eine zusammengehörige Shape-Sammlung einen "Buffer" darstellt? (m_shape und m_buffer sind dann irreführend und sollten eigentlich m_front und m_back heißen?)
Ja, da hast Du recht, wie gesagt mit den Begrifflichkeiten habe ich es wohl nicht so ganz getroffen. Es sind zwei Shapes, eines aktuell, eines vom vorherigen Zeitschritt.
Verstehe ich das richtig, dass du in diesem Fall alle Konfigurationsattribute (Radius etc.) bei Veränderung einfach blockweise in Front UND Back aktualisierst? [...] die Pufferung aller anderen Attribute macht dein Programm nur unnötig kompliziert.
Ja, weil ich die Shapes (also das Interface und die abgeleiteten Klassen) auch ungepuffert in anderen Programmteilen verwenden möchte. In dem Fall finde ich es nicht unnötig kompliziert, sondern eher übersichtlich und modular.
Mir ist noch immer nicht klar, was du mit dieser Pufferung schlussendlich bezweckst.
Multithreading ist Physik-intern zurzeit nicht geplant, die Unterteilung ist momentan zwischen Physik, Graphik, Input, ...
Der Zweck der Pufferung ist zunächst, dass ich den vorherigen Zeitschritt beispielsweise für die Kollisionsabfrage benötige. Die physikalischen Konfigurationsattribute sind in einer anderen Klasse, die Shapes sind reine Geometrie. Vielleicht mal zur Verdeutlichung:

Es gibt unter vielen anderen folgende Hierarchien:

Objekt
->Body->Rigidbody

Integrator
->AdamsMoulton
->RungeKutta
...

Jedes Objekt hat beispielsweise einen Integrator für die Position, (je nach Integratortyp speichert dieser vorherige Zeitschritte intern). Jeder "Body" hat zusätzlich einen Integrator für Ausrichtung/Orientierung.
Jedes Objekt ab der Ebene "Body" hat eine Geometrie, die ihrerseits aus einer Liste von (double-buffered) Shapes besteht. Nur die Geometrie ist für sich gepuffert, andere Attribute des Objektes sind es nicht oder haben, wie die Position und Ausrichtung, ihre eigenen, jeweils benötigten Puffer.

Edit: Grund für diesen Aufbau war, dass ich innerhalb der Objekte keinerlei Implementierungsdetails, wie double-buffering der Shapes oder vergangene und Zwischen-Zeitschritte bei der Integration, nach außen führen möchte. "Außen" ist in diesem Fall das physikalische Objekt.
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [gelöst] Interface-Kapselung

Beitrag von CodingCat »

smurfer hat geschrieben:Der Zweck der Pufferung ist zunächst, dass ich den vorherigen Zeitschritt beispielsweise für die Kollisionsabfrage benötige. Die physikalischen Konfigurationsattribute sind in einer anderen Klasse, die Shapes sind reine Geometrie.
[...]
Jedes Objekt ab der Ebene "Body" hat eine Geometrie, die ihrerseits aus einer Liste von (double-buffered) Shapes besteht. Nur die Geometrie ist für sich gepuffert, andere Attribute des Objektes sind es nicht oder haben, wie die Position und Ausrichtung, ihre eigenen, jeweils benötigten Puffer.
Jetzt frage ich mich erst recht, wieso du die Geometrie pufferst, wo du sogar schon eine entsprechende Teilung in Bodys und Shapes hast. Die Geometrie dürfte sich im Rahmen der Simulation doch überhaupt nie verändern?
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
smurfer
Establishment
Beiträge: 198
Registriert: 25.02.2002, 14:55

Re: [gelöst] Interface-Kapselung

Beitrag von smurfer »

Ich befinde mich im 2-Dimensionalen. Die Geometrie für die Kollision ist die gleiche wie die, die für die letztliche Darstellung verwendet wird (es gibt völlig unabhängige "Visuals", die Zugriff auf die physikalische Geometrie haben), das nur nebenbei, ich weiß nicht, ob es hilft. Mag sein, dass der Groschen bei mir noch nicht gefallen und mein Design tatsächlich sehr abstrus ist.

Theoretisch könnten sich die Geometrien auch ändern (Softbodys), aber das lassen wir mal außen vor. Natürlich ist die Geometrie in ihrem lokalen Objektkoordinatensystem soweit konstant, bei der Kollisionsabfrage müssen jedoch die Geometrien aller beteiligten Objekte im gleichen Koordinatensystem (z.B. Weltkoordinaten) sein (die Kollisionsabfrage ist im Übrigen prinzipiell CCD, also kontinuierlich, in 2D kann ich mir das erlauben ;) ). Das könnte ich natürlich entsprechend zur Zeit der Kollisionsabfrage on-the-fly-berechnen. Da ich aber davon ausgehe, die Geometrie auch noch an anderer Stelle zum vergangenen Zeitschritt zu benötigen, wird sie einmalig transformiert und dann entsprechend gespeichert (der Puffer eben).
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [gelöst] Interface-Kapselung

Beitrag von CodingCat »

smurfer hat geschrieben:Theoretisch könnten sich die Geometrien auch ändern (Softbodys), aber das lassen wir mal außen vor.
Ja, denn das wirst du dann ganz sicher nicht im Rahmen deiner Geometrie-Interfaces lösen.
smurfer hat geschrieben:Da ich aber davon ausgehe, die Geometrie auch noch an anderer Stelle zum vergangenen Zeitschritt zu benötigen, wird sie einmalig transformiert und dann entsprechend gespeichert (der Puffer eben).
Dann speicherst du diese transformierte Geometrie des letzten Zeitschritts eben unabhängig von der Konfiguration, denn genau das ist sie ja, unabhängig von der aktuellen Konfiguration. (Ich denke nicht, dass du bei Veränderung der Konfiguration direkt die aktuelle Geometrie neu transformierst?)

Warum nicht folgendes einfache Layout:

Code: Alles auswählen

struct ShapeInSimulation
{
    Shape local; // Die lokale Geometrie.
    Shape prevTransformed; // Die vortransformierte Geometrie im letzten Zeitschritt.
    Shape nextTransformed; // Die vortransformierte Geometrie im nächsten Zeitschritt.
    Material *material; ... // Weitere Konfiguration wie physikalische Materialeigenschaften, Mesh-Referenzen etc.
};
prevTransformed und nextTransformed aktualisierst du im Integrationsschritt, indem du die beiden vertauschst und in nextTransformed das neue transformierte local Shape berechnest. Alle anderen Eigenschaften bleiben unberührt. Ob dir die Einführung von zusätzlichen Zeigern für die Vertauschung von prevTransformed/nextTransformed Vorteile gegenüber einer einfachen Speicherkopie von nextTransformed zu prevTransformed bringt, solltest du messen. Es besteht die Gefahr, dass dich dadurch zerstörte Aliasing-Annahmen wesentlich mehr kosten als die Kopie.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [gelöst] Interface-Kapselung

Beitrag von CodingCat »

Nebenbei bemerkt: Die Auslagerung von prevTransformed und nextTransformed in eigene parallele Arrays würde das Vertauschen von Zeigern natürlich wieder attraktiv machen, weil du dann mit einem Zeiger-Swap alle vortransformierten Shape-Daten auf einmal vertauscht hast. Die Adressierung dieser Daten müsste dann jedoch über relative Indizes an Stelle absoluter Zeiger erfolgen.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
smurfer
Establishment
Beiträge: 198
Registriert: 25.02.2002, 14:55

Re: [gelöst] Interface-Kapselung

Beitrag von smurfer »

Vielen Dank zunächst einmal für die Vorschläge und das Interesse.
Ich denke nicht, dass du bei Veränderung der Konfiguration direkt die aktuelle Geometrie neu transformierst?
Da denkst Du richtig :)
Warum nicht folgendes einfache Layout:
Wenn ich es richtig interpretiere, entspricht ShapeInSimulation im Prinzip meinem DoubleBufferedShape, mit dem entscheidenden Unterschied der gemeinsamen Attribute (Material) in Deinem Vorschlag. Wie gesagt möchte ich das Shape, inklusive seiner Attribute, noch einzeln nutzen, dann würde ich bei so etwas landen:

Code: Alles auswählen

struct Shape
{
    Geom local;
    Geom transformed;
    Mat* material;
}
und

Code: Alles auswählen

struct BufferedShape
{
    Geom local;
    Geom prevTransformed;
    Geom nextTransformed
    Mat* material;
}
Zuletzt geändert von smurfer am 04.03.2013, 17:44, insgesamt 1-mal geändert.
smurfer
Establishment
Beiträge: 198
Registriert: 25.02.2002, 14:55

Re: [gelöst] Interface-Kapselung

Beitrag von smurfer »

CodingCat hat geschrieben:Nebenbei bemerkt: Die Auslagerung von prevTransformed und nextTransformed in eigene parallele Arrays würde das Vertauschen von Zeigern natürlich wieder attraktiv machen, weil du dann mit einem Zeiger-Swap alle vortransformierten Shape-Daten auf einmal vertauscht hast. Die Adressierung dieser Daten müsste dann jedoch über relative Indizes an Stelle absoluter Zeiger erfolgen.
Vollkommen richtig, das war auch meine initiale Lösung, bis ich auf einzelne Attribute zugreifen wollte. Dann bin ich auf die Variante mit gekapselten Shapes übergegangen, ganz optimal ist es aber wie zu sehen auch noch nicht.
Zuletzt geändert von smurfer am 04.03.2013, 17:41, insgesamt 1-mal geändert.
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [gelöst] Interface-Kapselung

Beitrag von CodingCat »

Oder:

Code: Alles auswählen

struct BufferedShape
{
    Shape currentShape;
    Geom prevTransformed;
};
Oder:

Code: Alles auswählen

struct BufferedShape : Shape
{
    Geom prevTransformed;
};
Diese Pseudo-Code-Beschreibungen lassen sich auch als Schnittstellen lesen und intern sinnvoll in mehrere Arrays aufteilen, siehe hierzu: Datenorientierung. Die gerade gezeigten "Alternativen" sollen dir in erster Linie zeigen, dass du in deinem Programm nicht zwangsläufig den Zustand verdoppeln musst, nur um eine bestimmte Schnittstelle zu erhalten. Überhaupt sollte Daten-Layout nicht durch die Schnittstelle diktiert werden, sondern in kritischen Fällen allenfalls die Schnittstelle durch das Daten-Layout.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
smurfer
Establishment
Beiträge: 198
Registriert: 25.02.2002, 14:55

Re: [gelöst] Interface-Kapselung

Beitrag von smurfer »

Danke für den Link, klingt gut, werde ich mir mal in Ruhe anschauen.
Es besteht die Gefahr, dass dich dadurch zerstörte Aliasing-Annahmen wesentlich mehr kosten als die Kopie.
Ich bin mir nicht sicher, in welchem Kontext Du von Aliasing sprichst -- Abtastfrequenz Physikengine?!
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [gelöst] Interface-Kapselung

Beitrag von CodingCat »

Aliasing ist leider ein vielverwendeter Begriff. Ich rede von Zeiger/Address-Aliasing. Grob gesagt: Hast du zwei Zeiger desselben Typs, dann weiß der Compiler bei Dereferenzierung in sehr vielen Fällen nicht, ob diese Zeiger auf dasselbe oder auf unterschiedliche Objekte zeigen. Wird über einen der beiden Zeiger geschrieben und über den anderen gelesen, muss der Compiler für vollständige Korrektheit vom Schlimmsten ausgehen, nämlich dass die Schreiboperation über den einen Zeiger den über den anderen Zeiger gelesenen Speicherbereich verändert hat. Hast du nun viele Schreib-/Lese-Operationen hintereinander, muss der Compiler im schlimmsten Fall für jede dieser Operationen vollständige Speicherzugriffe generieren, anstatt mehrere dieser Operationen zu einem einzigen Speicherzugriff zu verschmelzen, Daten in Registern vorzuhalten oder Nebenrechnungen ganz in Registern durchzuführen.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
smurfer
Establishment
Beiträge: 198
Registriert: 25.02.2002, 14:55

Re: [gelöst] Interface-Kapselung

Beitrag von smurfer »

Das erklärt, warum ich Kosten nicht ganz mit (zeitlichen) Aliasing-Effekten im Zusammenhang mit der Abtastfrequenz der Physikengine in Verbindung bringen konnte. Grafik hatte ich schon ausgeschlossen, aber der Begriff im Kontext von Zeigern und Adressierung war mir bislang nicht bekannt. Vielen Dank für die Erläuterung.
Antworten