[gelöst] Klassendesign-Problem



  • Hallo,

    ich möchte ein 2d-Optik-Simulationsprogramm entwerfen, also Lichtstrahlen, Reflexion, Brechung. Ich habe mir überlegt, dass die Hauptklasse des ganzen Programms wohl die "Strecke" sein wird, weil es hauptsächlich um Schnittpunkte von Strecken geht.

    Jetzt möchte ich von der Klasse Strecke aber noch die Unterklassen "Spiegelstrecke", "Glasstrecke" und "Lichtstrecke" ableiten, da diese alle ähnliche Eigenschaften wie die Strecke haben, nur die Spiegelstrecke reflektiert Licht, die Glasstrecke bricht Licht, abhängig vom Brechindex und die Lichtstrecke hat natürlich auch andere Eigenschaften.

    Jetzt geht es bei Polygonen und Lichtstrahlen nicht um einzelne Strecken, sondern um Mengen von Strecken.
    Deshalb leite ich diese von einer Klasse "Streckenmenge" ab, welche Vectoren von Spiegelstrecke, Glasstrecke und Lichtstrecke hat.
    Und da liegt das Problem! Ich möchte in diese Klasse "Streckenmenge" nicht drei Vectoren
    vector< Glasstrecke > _glasstrecken ;
    vector< Spiegelstrecke > _spiegelstrecken ;
    vector< Lichtstrecke > _lichtstrecken ;
    schreiben, sondern alle Strecken in einem einzigen Vector sammeln, weil das einfach viel wartbarer ist, wenn noch neue Streckenarten hinzukommen.

    Geht das? Wenn nicht, wie würdet ihr dieses Design-Problem lösen?

    Danke,
    Thilo



  • Sofern alle 3 Typen die gleiche Schnittstelle unterstützen (vielleicht von der Konstruktion einmal abgesehen), würde ich diese in einen vector abbilden, dabei kann aber nicht mehr mit den Objekt, sondern nur mit Zeigern/Smartpointern bzw. boost::ptr_vector gearbeitet werden (von besitzenden rohen Zeigern rate ich ab).

    Des weiteren sollte dann in diesem Fall die Basisklasse über einen virtuellen Destruktor verfügen.



  • Du willst nicht in dieser Richtung weiter machen. Das wird deine Software nur gigantisch komplex machen. Benutze Vererbung nur wenn du musst, nicht wenn du kannst. Um die Frage zu beantworten: Mach ein vector<unique_ptr<Strecke>> und stopf da alle Strecken rein. Deine nächste Frage wird sein "Ich habe jetzt eine Strecke, wie komme ich an den 'richtigen' Typ?" Mögliche Antworten:
    -Gar nicht
    -indem du alle Spezialfunktionen in Strecke als virtuelle Funktion anbietest
    -indem du Tags speicherst und damit die Polymorphie umgehst
    -indem du mit fiesen dynamic_cast + if-Kaskaden den Typ rausfindest

    Wird alles kompliziert und langsam.

    Die gute Lösung ist nur eine einzige Klasse Strecke zu haben und dort ein Tag mit Spiegel, Glas oder Licht speicherst.



  • nwp3 schrieb:

    Die gute Lösung ist nur eine einzige Klasse Strecke zu haben und dort ein Tag mit Spiegel, Glas oder Licht speicherst.

    Find ich nicht.

    Wenn die Schnittstelle wirklich sauber und einheitlich ist, dann spricht eigentlich nichts gegen einen Vector mit Zeigern. Das wäre ja auch eigentlich wie im Lehrbuch eine Anwendung von Vererbung/Polymorphie.

    Aber eine Lösung, wie du vorschlägst, die etwas so aussieht, wäre alles andere als optimal:

    enum Tag
    {
    	Glass, Spiegel, Licht
    };
    
    class Strecke
    {
    	// ...
    	Tag tag;
    
    public:
    	void method1()
    	{
    		if ( tag == Glass )
    			doThis();
    		else if ( tag == Spiegel )
    			doThat();
    		// ...
    	}
    };
    

    Da können wir ja gleich in die Steinzeit zurückgehen.



  • Also ihr meint, ich sollte es so machen:

    In der Klasse "Streckenmenge":
    vector< Strecke& > _strecken ;

    wobei eine konkrete Strecke, z.B. Glasstrecke so hinzugefügt wird
    _strecken.push_back( Glasstrecke( ... ) ) ;

    Jetzt muss ich die Methoden, die in Glasstrecke gegenüber ihrer Basisklasse Strecke vorkommen, in Strecke aber virtuell deklarieren, oder? Oder sogar als abstrakt? Dann ist das eine dynamische Bindung und er such von Objekt-Typ der Strecke in _strecken aufwärts nach der Methode, also zuerst in "Glasstrecke", wenn eine solche in vector< Strecke& > aufgerufen wird.

    Ist das richtig?



  • Die Strecken garnicht unterscheiden, sondern nen physikalischen Parameter geben, nämlich den Brechungsindex. Das Interessante findet eh an den Übergängen statt.

    Wenn du den Brechungsindex, später noch von der Position abhängig machen willst ginge das auch. So könntest du irgendwann noch Phänomene wie Luftspiegelungen (also ne Fata Morgana) uvm. simulieren.



  • Aber eine "Lichtstrecke" oder "Spiegelstrecke" hat doch keinen Brechungsindex. Diesen wollte ich eben nur der Unterklasse "Glasstrecke" geben.



  • Thilo87 schrieb:

    Also ihr meint, ich sollte es so machen:

    In der Klasse "Streckenmenge":
    vector< Strecke& > _strecken ;

    wobei eine konkrete Strecke, z.B. Glasstrecke so hinzugefügt wird
    _strecken.push_back( Glasstrecke( ... ) ) ;

    Jetzt muss ich die Methoden, die in Glasstrecke gegenüber ihrer Basisklasse Strecke vorkommen, in Strecke aber virtuell deklarieren, oder? Oder sogar als abstrakt? Dann ist das eine dynamische Bindung und er such von Objekt-Typ der Strecke in _strecken aufwärts nach der Methode, also zuerst in "Glasstrecke", wenn eine solche in vector< Strecke& > aufgerufen wird.

    Ist das richtig?

    Ganz so nicht. Du kannst keinen vector von Referenzen haben. vector<Strecke> funktioniert auch nicht, denn dann ist dort nur Platz für die Strecke und nicht für zum Beispiel eine Glasstrecke. Das Problem nennt sich slicing. Damit du verschiedene Strecken in den vector kriegst ist die einfachste Möglichkeit einen vector<Stecke *> zu nehmen. Diesen kannst du dann mit push_back(new Glasstrecke); befüllen. Wenn du mit new etwas anlegst musst du es auch wieder mit delete löschen, das ist doof und wird oft vergessen und deswegen nimmt man lieber einen vector<unique_ptr<Strecke>>. unique_ptr<Strecke> ist fast dasselbe wie Strecke *, nur dass es am Ende die Strecke löscht. boost::ptr_vector wurde vorgeschlagen, da löscht der vector die Strecken am Ende, tut also eigentlich dasselbe. Ich ziehe die Standardbibliothek boost vor, die meisten sinnvollen Dinge wurden eh schon integriert.

    Deine Strecke sollte richtigerweise virtuelle Funktionen haben. Wenn eine virtuelle Funktion aufgerufen wird, dann wird geprüft, ob sie überschrieben wurde und stattdessen die entsprechend überschreibende Funktion aufgerufen. Das sieht ungefähr so aus:

    struct Strecke{
        virtual double getLength() const = 0; //pure virtual == abstrakt
    };
    
    struct Glasstrecke : Strecke{
        double getLength() const override{
            return 42;
        }
        virtual ~Strecke(){
        }
    };
    
    struct Lichtstrecke : Strecke{
        double getLength() const override{
            return 2;
        }
    }
    
    int main(){
        vector<unique_ptr<Strecke>> v;
        v.push_back(new Glasstrecke);
        v.push_back(new Lichtstrecke);
        for (const auto &s : v)
            cout << s.getLength() << '\n';
        //gibt aus:
        //42
        //2
    }
    

    Versuche dir anzugewöhnen override zu benutzen wenn du eine Funktion überschreiben willst. Solltest du dich vertippt haben und die Funktion überschreibt gar nichts, dann sagt dir der Compiler bescheid.

    Der virtuelle Destruktor in Strecke funktioniert genauso wie eine normale virtuelle Funktion. Am Ende von main wird v zerstört, v zerstört vorher seine Elemente die unique_ptr, die unique_ptr rufen delete auf, delete ruft den Destruktor von Strecke auf. Wenn dieser nicht virtuell ist, dann ist hier Schluss. Wenn er virtuell ist, dann wird der "richtige" Destruktor aufgerufen, nämlich der von Glasstrecke oder Lichtstrecke. Wenn diese etwas tun würden wäre das nötig.

    Auf lange Sicht solltest du dir NVI und type erasure ansehen, um zu versuchen die Probleme von Vererbung einzudämmen. So richtig hilft es aber nicht. 3 getrennte vectoren für die verschiedenen Strecken zu haben ist ein viel kleineres Übel als der Rattenschwanz an Problemen von Vererbung.



  • Ok, super. Danke für die ausführlichen Antworten! 🙂



  • Thilo87 schrieb:

    Aber eine "Lichtstrecke" oder "Spiegelstrecke" hat doch keinen Brechungsindex. Diesen wollte ich eben nur der Unterklasse "Glasstrecke" geben.

    Ne Strecke ist ne Strecke (und ich meine hier ein Kruve im Raum). Wo es lang geht wird durch den örtlich variierenden Brechungsindex bestimmt (plus Rand- oder Anfangsbedingungen). Den du wiederum mit deinem Aufbau aus Luft, Linsen und Spiegeln festlegst.



  • Ich meinte eine Strecke im mathematischen Sinne. Ein Ortsvektor p, ein Richtungsvektor q und die Strecke ist gegeben durch x = p + s*q, wobei s aus dem abgeschlossenen Intervall von 0 bis 1 ist.



  • Ja genau, wo steht das was von Glas, Spiegel oder Licht?


Anmelden zum Antworten