Design-Frage beim erstellen einer modularen Klasse



  • Hallo zusammen,

    hab eine Frage und zwar wie ich meine Klasse am besten designen sollte. Mein aktuelles Design hat nämlich einige Schwachstellen. Das Ganze basiert auch auf diesem Thread: http://www.c-plusplus.net/forum/321555.

    Folgendes: Ich habe eine Klasse Soldier.

    Dieser soll verschiedene Aktionen bekommen, etwa shoot, jump, etc. Wie mach ich das jetzt am besten wenn die verschiedenen Aktionen von mehreren Leuten gleichzeitig entwickelt werden sollen?

    Momentan arbeiten einfach alle direkt an der Soldier Klasse, was eine ziemliche Katastrophe ist (ständige Konflikte, etliche Zeilen Code da viele Aktionen recht komplex sind, etc.).

    Daher würde ich das gerne auslagern, etwa jede Aktion als eigene Klasse (so wie in dem Link oben beschrieben), was aber andere Probleme verursacht. Daher meine Frage: wie löst man sowas theoretisch am besten bzw. professionellsten in C++?

    Viele Grüße,
    hs



  • Ich würde vorschlagen, dass ihr euch Gedanken darüber macht, wie man das am besten zerrupft, die einzelnen Komponenten auf das Team verteilt, um es anschließend viel allgemeiner wieder zusammenzusetzen.

    Springen => dz/dt vom physikalischen Modell erhöhen
    Schießen => Waffe togglen

    Am Ende sind die Funktionen im "Soldier" nicht mehr als weiterleitende Stubbies. Und dann kommt man recht schnell auf den Gedanken, dass man diese Stubbies in einfache registrierte Funktoren überführt, sodass man dem Soldier beliebige Kommandos hinzufügen kann.

    Am Ende hat man dann soetwas wie: "OnLeftMouseDown => PlayerEntity.execute("shoot:start")"

    Damit hat man alles in unterschiedliche Systeme zerteilt, an denen die beteiligten Personen "unabhängig" entwickeln können.
    Wenn Klassen so groß werden, dass man sich Gedanken darüber macht, wie mehrere Entwickler gleichzeitig an ihnen arbeiten können, dann läuft etwas gewaltig schief!



  • GLaub für sowas ist eigentlich Component-Based Design ganz gut.
    http://www.raywenderlich.com/24878



  • Ja, komponentenbasiertes Design bietet sich oft an. Unterschätze aber nicht die Komplexität, wenn das ganze flexibel sein soll. Überlegt euch das Design zuerst in der Theorie, bevor ihr wild drauf los programmiert. Wichtig ist zum Beispiel, wie ihr Abhängigkeiten zwischen den einzelnen Komponenten löst, oder wie ihr auf Benutzereingaben reagiert. Die einzelnen Komponenten können Statusänderungen entweder selbst abfragen, darüber generell informiert werden, oder sich für spezifische registrieren. Je nach Wahl hast du andere Abhängigkeiten zwischen den einzelnen Modulen (kennt Komponente X die Komponente Y oder umgekehrt?)

    Und man sollte auch nicht in einen Rausch verfallen und plötzlich alles als Komponente modellieren. Es gibt genügend Klassen, die ausserhalb stehen.

    Eine mögliche Modellierung, hier etwas grösser (beachte, dass davon viele Variationen mit eigenen Vor- und Nachteilen existieren):

    class Entity
    {
    public:
        template <typename C>
        C& Get(); // Gibt Komponente C zurück, assert bei Nicht-Vorhandensein
    private:
        // ID kann z.B. std::type_index sein
        std::unordered_map<ID, std::unique_ptr<Component>> components;
    };
    
    class Component
    {
    public:
        virtual void Tick(float dt) = 0;
        virtual ~Component();
    };
    
    class Physics : public Component
    {
        Vector position
        Vector rotation;
    };
    
    class Weapon : public Component
    {
        WeaponType type;
        int ammo;
    };
    
    class Input : public Component
    {
        std::map<Event, std::function<...>> callbacks;
    };
    
    class Graphics : public Component
    {
        // Abstraktion deiner Grafik-Bibliothek: Daten/Funktionen pro Einheit
    };
    
    class World
    {
        std::vector<Entity> entities;
    };
    
    class InputHandler
    {
        // Abstraktion deiner Input-Bibliothek
    };
    
    class Renderer
    {
        // Abstraktion deiner Grafik-Bibliothek: Globale Daten/Funktionen
        // Zeichnet Graphics-Komponenten
    };
    
    class Application
    {
        World world;
        InputHandler input;
        Renderer renderer;
    };
    

    Du brauchst halt recht schnell viele Klassen, wenn du alles sauber trennen willst. Aber wenigstens bist du dann dein ursprüngliches Problem los :p
    Sonst kannst du natürlich auch Kompromisse eingehen. Lass dich von den Artikeln zu Component-Based Design inspirieren...



  • Hallo, danke für die Antworten! 🙂
    Werd mich mal ein bisschen in Komponentenbasiertes Design einlesen.



  • Hallo nochmal,

    hab mich jetzt ein bisschen über Komponentenbasiertes Design erkundigt. Hab auch schon was recht brauchbares hingekriegt. Aber eigentlich würde ich die Komponenten gerne noch mehr "absichern".

    Momentan mach ich ungefähr so etwas:

    soldier->getComponent<Damage>()->setDamage(10.0);
    

    In der getComponent Methode wird das entsprechende Objekt erstellt und intern gespeichert (in diesem Fall die Komponente "Damage"). Soweit so gut.

    Allerdings würde ich gerne verhindern dass der User selbstständig Komponenten erstellen kann. Momentan geht das einfach per:

    Damage my_damage;
    

    ,da ja der Konstruktor für die jeweilige Komponente public sein muss (da sonst kein Zugriff von der Soldier Klasse aus).

    Ich will aber dass die jeweiligen Komponenten nur im Kontext des jeweiligen Soldiers exisiteren können, und nur mittels der getComponent Methode aufgerufen (= damit erstellt) werden können.

    Ist das möglich? Wenn ja, wie?

    Viele Grüße,
    hs



  • absolut unnötig



  • Naja, was heißt unnötig.

    Ich finde es halt sehr verwirrend für den Programmierer, da der ganz normal vorhandene public Konstruktor ja suggeriert man kann das Objekt anlegen, und dass es auch Sinn machen würde dies zu tun...

    Auch enthält die Klasse Component ja einen (public) Zeiger auf den jeweiligen Soldaten, damit sie diesem auch Befehle erteilen kann. Da der Zeiger public sein muss kann man diesen auch etwa zu NULL setzten oder so. Sowas würde ich gerne prinzipiell verhindern.



  • Ich kann jetzt nicht beurteilen ob die public Back-Reference nötig ist, aber wenn sich jemand in den Fuß schießen möchte, dann kann er das immer, egal wie sehr man sich anstrengt. Ich finde solche Leute rechtfertigen das erhöhte Rauschen durch irgendwelche ansonsten unnötigen defensiven Maßnahmen im Code nicht.



  • Hm, ok dann werd ich das wohl so lassen. Eigentlich ist es ja schon ziemlich sicher so^^



  • Btw, falls es jemand interessiert hab ich doch noch eine Lösung für das Problem gefunden: Die gespeicherte Referenz auf den Soldaten einfach const machen und dann jeweils per const_cast nur innerhalb der Soldier Klasse zuweisen.

    Damit kann meine Soldier Klasse intern die jeweilige Referenz zuweisen und nach außen hin ist selbige konstant und kann daher nicht verändert werden. Damit simuliert man quasi ein readonly field 🙂



  • Ich glaube nicht, dass es dadurch auch nur irgendwie weniger verwirrend wird. Und warum sollte der User dann nicht auch einen const_cast machen?

    const_cast sollte vermieden werden. wer das braucht, macht in 99.9% der Fälle etwas gehörig falsch (wie in deinem Fall: viel zu defensives Design).



  • Warum viel zu defensives design? Ich will halt dass der thisSoldier pointer sich wie der this pointer nicht beschreiben lässt. Wär doch blöd wenn jemand auf die (zugegeben dumme) Idee kommt diesem pointer mal nen Wert zuzuweisen. Macht das ganze System kaputt und ist extrem schwer nachzuvollziehen bei der Fehlersuche.

    Klar kann der User auch einen const_cast machen. Aber das kann er immer, bei jedem konstanten Wert, demzufolge könnte man const auch gleich immer weglassen nach der Logik. Wenn der Compiler dem Benutzer aber sagt "Hey, dieser Wert ist konstant" dann kommts ihm wahrscheinlich von allein dass er das nicht machen sollte.

    Ich bin natürlich gerne offen für eine bessere Lösung. Hier die Ausgangslage:

    soldier->getComponent<ComplicatedAction>()->doMyAction();

    ComplicatedAction soll an den Soldaten Befehle delegieren können, braucht also eine Referenz auf den callenden Soldaten, thisSoldier. Diese Referenz sollte const sein.



  • benutz doch einfach einen public constructor in allen deinen Aktionen, der einen pointer auf soldier entgegennimmt und dann automatisch dafür sorgt, dass das setup korrekt ist.

    der zeiger selbst ist dann private.

    //edit eigentlich schlage ich dir _dieselbe_ Lösung wie in deinem ersten Thread vor.

    class Soldier{
    
    public:
        template<class Component>
        Component getComponent(){
            return Component(this);//oder was auch immer du hier machst. don't care.
        }
    };
    
    class Foo
    {
        Soldier* mySoldier;
        public:
            Shoot(Soldier* soldier):mySoldier(soldier){}
            void doMyAction(){
                 ...
            }
    };
    
    Soldier soldier;
    soldier.getComponent<Foo>().doMyAction();
    

    Alles weitere wie absichern, dass nur instanzen von Foo innerhalb von Soldier erstellt werden können ist unnötig. Aus irgendeinem Grund reicht dir ja kein statisches System sondern du willst es - waurm auch immer - dynamisch haben. Dann kann man genausogut argumentieren, dass Componenten auch von aussen hinzugefügt werden können sollen.

    //edit Alls in allm ist das aber unnötig. Der Ansatz mit Komponenten ist langsam und macht es nicht einfacher(nun müsst ihr halt überall durchtesten oder sicherstellen, dass die Komponenten verfügbar sind und das geht nicht zur Compilezeit sondrn nur zur Laufzeit. Dazu kommen die 13 Millionen indirektionen und am Ende wird es kompliziert nachzuvollziehen, wie sich der Code verhält.

    //edit25 oder so: Ich stelle jtzt einfach mal die richtige Frage: Was ist das Designproblem, welches ihr lösen wollt?



  • otze schrieb:

    //edit eigentlich schlage ich dir _dieselbe_ Lösung wie in deinem ersten Thread vor.

    Da hast du aber noch gesagt dass das eine schlechte Lösung ist bzw. hat es mein ursprüngliches Problem nicht gelöst:

    otze schrieb:

    warum igentlich so ein komisches klassendesign?

    soldier.shoot();//Bäm!
    

    Das ist am Problem vorbei. Schließlich muss dafür jeder der eine Aktion entwirft diese in die Soldier Klasse implementieren. Daraus folgt dass mehrere Leute an derselben Klasse arbeiten. Genau das will ich vermeiden.

    otze schrieb:

    oder auch, wenn man halt komplizierten, langsamen Code mag:
    ....
    Shoot shot(&soldier);
    shot.aaaaandAction();
    [/code]

    Ich mag keinen komplizierten und langsamen Code, aber er soll mein Problem lösen. Ich will die Aktion auch an dem Soldier Objekt aufrufen und nicht an der Aktion selbst. Das ist nicht intuitiv und fehleranfällig, da ich mir immer merken muss welches shot Objekt zu welchem soldier Objekt gehört.

    Shoot shot1(&soldier1);
    Shoot shot2(&soldier2);
    Shoot shot3(&soldier3);
    Shoot shot4(&enemySoldier1);
    Shoot shot5(&enemySoldier2);
    Shoot shot6(&enemySoldier3);
    
    // 200 Zeilen Code
    
    // Zu wem gehört shot5 noch gleich?
    shot5.aaaaandAction();
    

    otze schrieb:

    //edit Alls in allm ist das aber unnötig. Der Ansatz mit Komponenten ist langsam und macht es nicht einfacher(nun müsst ihr halt überall durchtesten oder sicherstellen, dass die Komponenten verfügbar sind und das geht nicht zur Compilezeit sondrn nur zur Laufzeit. Dazu kommen die 13 Millionen indirektionen und am Ende wird es kompliziert nachzuvollziehen, wie sich der Code verhält.

    Versteh ich nicht. Die Zeile soldier.getComponent<Foo>().doMyAction() gibt bei mir einen Compile-Error wenn Foo nicht vorhanden ist, funktioniert also nur wenn die Komponente verfügbar ist.

    otze schrieb:

    //edit25 oder so: Ich stelle jtzt einfach mal die richtige Frage: Was ist das Designproblem, welches ihr lösen wollt?

    Hab ich doch schon mehrfach gesagt. Eine Klasse (Soldier) an der mehrere Leute arbeiten. Diese sollen unabhängig voneinander neue Komponenten zu dieser Klasse hinzufügen können ohne die die Soldier Klasse verändern zu müssen.



  • [quote="happystudent"]

    otze schrieb:

    Da hast du aber noch gesagt dass das eine schlechte Lösung ist bzw. hat es mein ursprüngliches Problem nicht gelöst:

    Ja. Schlecht. Aber immernoch viel besser, als das was du vorgeschlagen hattest.

    Das ist am Problem vorbei. Schließlich muss dafür jeder der eine Aktion entwirft diese in die Soldier Klasse implementieren. Daraus folgt dass mehrere Leute an derselben Klasse arbeiten. Genau das will ich vermeiden.

    Das ist doch kein Argument. Ein Argument wäre: "Die Klasse ist offensichtlich von uns zu groß designed, sodass es erforderlich ist, dass alle gleichzeitig daran rumprogrammieren müssen". Was du gerade machst, ist am Problem rumdoktorn, aber lösen tust du es nicht.

    Das Ganze nennt man auch monolith und ist ein Anti-Pattern. Aber hör nicht auf dich, hör auf die Gurus:

    http://www.gotw.ca/gotw/084.htm

    Du brauchst das Component Pattern nur, wenn du dynamisch neue Komponenten hinzufügen musst (im Sinne von: du lädst eine Datei die dir sagt, welche Komponenten eine Objektinstanz braucht).

    Wenn es nur darum geht, dass du die Funktionalität der Klasse in verschiedene Objekte auslagern willst, warum dann so kompliziert?

    class Spieler{
    private:
        Component1 comp1;
        ...
        ComponentN compN;
    public:
        void doAction1(){
            comp1.doAction();
        }
        ...
        //oder sogar:
        Component1& getComponent1(){ return comp1;}
    };
    

    Es löst noch immer nicht das Problem deines monolithen, aber so ist zumindest das was du willst, trivial umsetzen und der "User" der Klasse muss sich nicht damit herumschlagen, dass er den expliziten Namen jeder Komponente braucht.

    Auf den Rest gehe ich mal nicht ein, weil du nicht verstanden hast, dass ich meinen ursprünglichen Vorschlag sogar genau auf dein Callschema angepasst hab. Ein wenig selber nachdenken darfst du schon.



  • otze schrieb:

    Das ist doch kein Argument. Ein Argument wäre: "Die Klasse ist offensichtlich von uns zu groß designed, sodass es erforderlich ist, dass alle gleichzeitig daran rumprogrammieren müssen". Was du gerade machst, ist am Problem rumdoktorn, aber lösen tust du es nicht.

    Äh, nö. Das Problem ist eigentlich schon gelöst, schließlich sind die Komponenten mittlerweile ausgelagert und jeder kann an seiner Komponente unabhängig arbeiten. Die eigentliche Soldier Klasse enthält nichtmal 200 Zeilen Code. Wo das ein Monolith sein soll erschließt sich mir beim besten Willen nicht.

    otze schrieb:

    Wenn es nur darum geht, dass du die Funktionalität der Klasse in verschiedene Objekte auslagern willst, warum dann so kompliziert?

    class Spieler{
    private:
        Component1 comp1;
        ...
        ComponentN compN;
    public:
        void doAction1(){
            comp1.doAction();
        }
        ...
        //oder sogar:
        Component1& getComponent1(){ return comp1;}
    };
    

    Es löst noch immer nicht das Problem deines monolithen, aber so ist zumindest das was du willst, trivial umsetzen und der "User" der Klasse muss sich nicht damit herumschlagen, dass er den expliziten Namen jeder Komponente braucht.

    Ich sehe nicht wo das mein Problem löst. Wenn eine neue Komponente hinzugefügt werden soll muss diese ja wieder in die Spieler Klasse eingtragen werden. Was soll das dann für einen Vorteil bringen? Was bringt es mir Komponenten zu haben wenn ich diese genau wie Methoden in die Header Datei eintragen muss? Dann kann ich doch genauso gut direkt die Methoden implementieren!

    otze schrieb:

    Auf den Rest gehe ich mal nicht ein, weil du nicht verstanden hast, dass ich meinen ursprünglichen Vorschlag sogar genau auf dein Callschema angepasst hab. Ein wenig selber nachdenken darfst du schon.

    Das Callschema ist absolut zweitrangig für mich. Wichtig ist dass die Spieler Klasse nicht angefasst werden muss um eine Komponente hinzuzufügen.



  • dann solltet ihr vielleicht mal als sourceverwaltung anfangen svn oder git zu benutzen. Also irgendwas was auch automatisch mergen kann. Euer Problem ist gelöst, insbesondere wenn es um sowas einfaches wie Einfügen geht.

    Äh, nö. Das Problem ist eigentlich schon gelöst, schließlich sind die Komponenten mittlerweile ausgelagert und jeder kann an seiner Komponente unabhängig arbeiten. Die eigentliche Soldier Klasse enthält nichtmal 200 Zeilen Code. Wo das ein Monolith sein soll erschließt sich mir beim besten Willen nicht.

    Jetzt cheatest du aber, weil Anzahl der Zeilen und Aufgaben der Klasse in eurem Schema nicht mehr korrelieren(Du versuchst ja aktiv zu erzwingen, dass die einzelnen Komponenten nur für diese eine Klasse zu gebrauchen sind). Wieviele Komponenten gibt es denn?



  • otze schrieb:

    Jetzt cheatest du aber, weil Anzahl der Zeilen und Aufgaben der Klasse in eurem Schema nicht mehr korrelieren(Du versuchst ja aktiv zu erzwingen, dass die einzelnen Komponenten nur für diese eine Klasse zu gebrauchen sind).

    Hm, aber es ist doch so wenn ich nur eine Komponente hinzufüge (z.B. "SayHelloWorld") hat die Soldier Klasse auch nur ihr Grundgerüst und eben die eine Komponente welche "Hello World" ausgeben kann.

    otze schrieb:

    Wieviele Komponenten gibt es denn?

    Hm, das ist schwer zu sagen, weil überhaupt noch nicht klar ist wie "agil" die Figuren sein sollen, also wieviele "Special-Moves" sie haben. Aber schon recht viele, so 20 bis 30 vielleicht?



  • happystudent schrieb:

    Hm, das ist schwer zu sagen, weil überhaupt noch nicht klar ist wie "agil" die Figuren sein sollen, also wieviele "Special-Moves" sie haben. Aber schon recht viele, so 20 bis 30 vielleicht?

    Und ab wieviel Komponenten wäre es ein Monolith? 5? 6? 30? 100? 1000? Ist es richtig, dass so viele Dinge an ein Objekt gebnden werden? Gibt es da keine anderen Objekte? Ich kenne euer spiel nicht, aber das sind so die Fragen, die ich mir stelle. Du musst dir einfach nur überlegen, wie das aussähe, wenn ihr alle Methoden in die Klasse schreiben würdet und nicht so "versteckt".


Anmelden zum Antworten