Klassenschachtelung für Operatorüberladung ohne Dynamikverlust



  • Nexus schrieb:

    Reth schrieb:

    Wie machst Du es dann, wenn Du änderbare Objekte hast? Gibst Du nur Zeiger darauf zurück anstelle von Referenzen (z.B. wenn die Gebäudefactory ein neues Gebäude erzeugt hat, muss sich dessen Zustand ja auch von außen ändern lassen - es sei denn die Factory ist mehr als nur ne Fabrik, nämlich ein Manager, der auch den Zustand der produzierten Gebäude ändern kann!)?

    Das Problem ist, dass die Rückgabe von Non-Const-Referenzen in vielen Fällen die Kapselung verletzt und die Aufteilung in Getter und Setter hinfällig macht, weil man Objekte auch über den Getter verändern kann.

    Naja, nur indirekt. Der Getter liefert Dir ein Member der Klasse, oder einen skalaren Wert, ein Array, ... Auf ein so zurück geliefertes Objekt kannst Du dann dessen Setter aufrufen.

    Nexus schrieb:

    Eine Factory-Methode ist ja was anderes, da sie Objekte erzeugt. Diese Objekte gehören nicht zu privaten Implementierungsdetails, die gekapselt sein müssen. Allerdings ist "Get" bei einer Factory ohnehin unangebracht. Gebräuchlich ist z.B. "Create". Als Rückgabetyp kann man bei kopierbaren Objekte direkt eine Kopie nehmen, oder einen besitzenden Zeiger, oder einen Smart Pointer wie std::auto_ptr .

    Richtig. Bei meinen Factoryklassen gebe ich bisher Zeiger oder Referenzen auf die angelegten Objekte raus. Natürlich achte ich auch darauf, die Kapselung nicht zu verletzen, d.h. Member der Klassen nicht herauszugeben. Allerdings werden in manchen Klassen Objekte als Member benötigt, die dann an Methoden anderer Klassen übergeben werden. Z.B. gibt es in meiner Hauptklasse ein Cursor-Objekt, dass in einer Factory-Klasse erzeugt wurde (da es analog zu anderen animierten Objekten ist). Dieses Objekt wird aber an Methoden bzw. Konstruktoren anderer Klassen übergeben, da dort sein Verhalten beeinflusst wird (z.B. an ein Aktivitätsobjekt, das an einen Button gehängt wird, bei dessen Betätigung es das Aussehen des Cursors ändert und diesen aktiviert/deaktiviert).

    Nexus schrieb:

    Falls du an eine Factory denkst, die die erzeugten Objekte intern abspeichert, dann ist die Rückgabe von Referenzen durchaus angebracht.

    Das machen meine Factories bisher.

    Nexus schrieb:

    Ich bezog mich hauptsächlich auf Methoden, welche direkt Attribute (Membervariablen) der Klasse zurückliefern. Dort hat ein Getter mit einer Non-Const-Referenz als Rückgabetyp keinen grossen Vorteil gegenüber einem öffentlichen Attribut.

    Das ist richtig.

    Nexus schrieb:

    Reth schrieb:

    Das Dachte hier eher an ne Möglichkeit, das Costs-Objekt so ähnlich wie ein Array zu initialisieren, aber das scheidet wohl aus!

    Wie stellst du dir das konkret vor? Möglich ist es grundsätzlich schon...

    Hm, keine Ahnung, einfach die Cost- bzw. RessourceAmount-Klasse, so wie sie von Dir in den Codebeispielen beschrieben ist hernehmen und Objekte davon so anlegen, dass das interne Array bereits mit den Fixkosten initialisiert ist, die Du in den Settern gesetzt hast? Da fehlt mir wie gesagt die Erfahrung, welche Möglichkeiten C++ hier bietet.

    Nexus schrieb:

    Reth schrieb:

    So war meine Idee: Jedes Gebäudeobjekt bekommt ne konstante Referenz auf seine Reparaturkosten (die Baukosten muss es ja nicht kennen, die werden vor seiner Erzeugung geprüft).

    Ja, das kannst du so machen. Ich würde einfach dafür sorgen, dass die Schnittstelle bei Bauen und Reparieren ähnlich ist. Vielleicht bietet es sich auch an, für die Reparatur trotzdem die Factory zu verwenden – semantisch macht es nämlich mehr Sinn, wenn sich ein Gebäude nicht selbst repariert. Dann hast du eben eine Fabrik und Reparaturwerkstatt in einem. 😉

    Stimmt! Allerdings ist mir eingefallen, dass meine Buttons ja in Abhängigkeit von der Ressourcenanzahl des Spielers de-/aktiviert werden! D.h., ich müsste die Prüfung, ob der Spieler noch genug Ressourcen zum Bau eines Gebäudes hat in der Gebäudefactory-Klasse machen, oder an einer ganz eigenen Stelle, die dann vom Button und der Gebäudefactory-Klasse genutzt wird. Das gleiche dann auch für die Kosten für andere Aktionen! Wird vom Klassengeflecht dann schon komplizierter und das Ressourcenobjekt des Spielers würde auch durchgereicht werden müssen!

    Nexus schrieb:

    Übrigens, der Begriff "konstante Referenz" wird von vielen Leuten falsch verwendet. Eine Referenz in C++ ist immer konstant – worauf sich das "konstant" bezieht, ist das referenzierte Objekt. Deshalb spreche ich meistens von "Const-Referenz" oder "Referenz auf const ".

    Richtig! Mea Culpa! Da kommt neben Ungewohntheit auch noch Tippfaulheit hinzu! 🙂



  • Reth schrieb:

    Naja, nur indirekt. Der Getter liefert Dir ein Member der Klasse, oder einen skalaren Wert, ein Array, ... Auf ein so zurück geliefertes Objekt kannst Du dann dessen Setter aufrufen.

    Ja, aber man kann mit dem zurückgegebenen Objekt anstellen, was man will, sofern es nicht const -qualifiziert ist. Von daher würde ich im Allgemeinen bei klassischen Getter-Funktionen entweder Kopien oder Const-Referenzen zurückgeben. (Übrigens: In C++ haben skalare Datentypen keine Methoden und rohe Arrays kann man nicht zurückgeben ;))

    Reth schrieb:

    Hm, keine Ahnung, einfach die Cost- bzw. RessourceAmount-Klasse, so wie sie von Dir in den Codebeispielen beschrieben ist hernehmen und Objekte davon so anlegen, dass das interne Array bereits mit den Fixkosten initialisiert ist, die Du in den Settern gesetzt hast? Da fehlt mir wie gesagt die Erfahrung, welche Möglichkeiten C++ hier bietet.

    Postcondition der statischen InitializeCosts() -Methode ist bereits, dass das interne Array (bzw. der Container) mit allen Werten initialisiert ist. Oder abstrakter: Dass man in Zukunft über bestimmte Methoden die Kosten abfragen kann (es muss ja nicht unbedingt ein Array verwendet werden).

    Wie die Initialisierung vonstatten geht, ist eigentlich ein Implementierungsdetail. Du kannst das über die SetAmount() -Methoden machen, eine std::map verwenden, direkt ein Array füllen, etc.

    Reth schrieb:

    Stimmt! Allerdings ist mir eingefallen, dass meine Buttons ja in Abhängigkeit von der Ressourcenanzahl des Spielers de-/aktiviert werden! D.h., ich müsste die Prüfung, ob der Spieler noch genug Ressourcen zum Bau eines Gebäudes hat in der Gebäudefactory-Klasse machen, oder an einer ganz eigenen Stelle, die dann vom Button und der Gebäudefactory-Klasse genutzt wird. Das gleiche dann auch für die Kosten für andere Aktionen! Wird vom Klassengeflecht dann schon komplizierter und das Ressourcenobjekt des Spielers würde auch durchgereicht werden müssen!

    Wie handhabst du es denn jetzt? Wo prüfst du die für Buttons relevante Bedingung?



  • Nexus schrieb:

    Übrigens: In C++ haben skalare Datentypen keine Methoden

    Ich weiss, darum schrieb ich "Auf ein so zurück geliefertes Objekt"

    Nexus schrieb:

    und rohe Arrays kann man nicht zurückgeben 😉

    Nur Zeiger auf Arrays, oder?

    Nexus schrieb:

    Postcondition der statischen InitializeCosts() -Methode ist bereits, dass das interne Array (bzw. der Container) mit allen Werten initialisiert ist.

    Dann würde ich die Methode aber private machen und ggf. nur im Konstruktor der Factory aufrufen. Sonst kann man die Kosten ja von außen öfters neu initialisieren.

    Nexus schrieb:

    Oder abstrakter: Dass man in Zukunft über bestimmte Methoden die Kosten abfragen kann (es muss ja nicht unbedingt ein Array verwendet werden).

    Das verstehe ich im Zshg. mit dem Initialisieren nicht. Das Abfragen bestimmter Kosten wäre für mich eh gesetzt gewesen.

    Nexus schrieb:

    Wie handhabst du es denn jetzt? Wo prüfst du die für Buttons relevante Bedingung?

    Das weiss ich leider noch nicht, da muss ich nochmal in mich gehen. Evtl. am Spielerobjekt, dass eine Methode bietet, die Auskunft gibt, ob er sich bestimmte Kosten leisten kann. Dann müsste ich die Kosten einzelner Aktionen ggf. aber auch an den Buttons hinterlegen und v.a. die Kosten in einer eigenen Klasse getrennt von der Gebäudefactory verwalten, da es ja noch andere Aktionen außer bauen und reparieren gibt!



  • Reth schrieb:

    Nur Zeiger auf Arrays, oder?

    Entweder Zeiger auf dynamisch angeforderte Arrays oder Wrapperklassen wie std::tr1::array .

    Reth schrieb:

    Dann würde ich die Methode aber private machen und ggf. nur im Konstruktor der Factory aufrufen. Sonst kann man die Kosten ja von außen öfters neu initialisieren.

    Wenn du den Zeitpunkt der Konstruktion genau kennst, ist der Konstruktor durchaus eine gute Möglichkeit. Aber sonst (bei Singletons etc.) ist es teilweise ratsamer, die Initialisierung manuell zu vollziehen, vor allem wenn Abhängigkeiten zwischen mehreren Klassen bestehen.

    Reth schrieb:

    Das verstehe ich im Zshg. mit dem Initialisieren nicht. Das Abfragen bestimmter Kosten wäre für mich eh gesetzt gewesen.

    Ich meinte damit nur, dass die Aufgabe der Initialisierungsfunktion (oder eben des Konstruktors) darin besteht, alle Kosten bereitzustellen, sodass man sie nach dem Ende der Funktion abfragen könnte. Also die Postcondition.

    Reth schrieb:

    Das weiss ich leider noch nicht, da muss ich nochmal in mich gehen. Evtl. am Spielerobjekt, dass eine Methode bietet, die Auskunft gibt, ob er sich bestimmte Kosten leisten kann. Dann müsste ich die Kosten einzelner Aktionen ggf. aber auch an den Buttons hinterlegen und v.a. die Kosten in einer eigenen Klasse getrennt von der Gebäudefactory verwalten, da es ja noch andere Aktionen außer bauen und reparieren gibt!

    Ich bin mir nicht sicher, wie gut sich das in dein momentanes Design eingliedern lässt, aber kennst du das Observer-Pattern? Damit könntest du diverse Instanzen von Ressourcenänderungen benachrichtigen. Wenn das allerdings sehr oft vorkommt und meistens nichts geändert werden muss, könnte es auch angebracht sein, das Umgekehrte zu tun – also dass sich die Buttons etc. die benötigte Informationen jeweils abfragen. Mit einer abstrakten Zwischenschicht kannst du die Abhängigkeiten klein halten, sodass sich Sender und Empfänger nicht direkt kennen müssen.



  • Nexus schrieb:

    Ich bin mir nicht sicher, wie gut sich das in dein momentanes Design eingliedern lässt, aber kennst du das Observer-Pattern? Damit könntest du diverse Instanzen von Ressourcenänderungen benachrichtigen. Wenn das allerdings sehr oft vorkommt und meistens nichts geändert werden muss, könnte es auch angebracht sein, das Umgekehrte zu tun – also dass sich die Buttons etc. die benötigte Informationen jeweils abfragen. Mit einer abstrakten Zwischenschicht kannst du die Abhängigkeiten klein halten, sodass sich Sender und Empfänger nicht direkt kennen müssen.

    Observer-Pattern kenne ich, wäre eine Möglichkeit. Allerdings müsste ich dann Buttons an Objekte hängen, die mit ihnen nix zu tun haben (z.B. an den Spieler, da dessen Ressourcen ja für die De-/Aktivierung ausschlaggebend sind). Der andere von Dir beschriebene Fall würde sich auch bei wenig häufigen Änderungen nicht positiv bei mir auswirken, da die Buttons zeitlich prüfen würden, ob sie sich de-/aktivieren müssen oder nicht.
    Wie meinst Du den Teil mit der abstrakten Zwischenschicht konkret? Listener-Objekte o.ä., die sich dann als Observer eintragen, und die selbst wiederum die jeweiligen Buttons kennen? Wäre zumindest eine gute Möglichkeit, die Buttons nicht woanders bekannt machen zu müssen!



  • Reth schrieb:

    Wie meinst Du den Teil mit der abstrakten Zwischenschicht konkret? Listener-Objekte o.ä., die sich dann als Observer eintragen, und die selbst wiederum die jeweiligen Buttons kennen? Wäre zumindest eine gute Möglichkeit, die Buttons nicht woanders bekannt machen zu müssen!

    Ja, an sowas habe ich gedacht, um Sender und Empfänger zu entkoppeln.

    In Java nimmt implementiert man dafür normalerweise Interfaces. In C++ kann man das auch machen, es gibt aber auch eine direkte Möglichkeit für Callbacks. Entweder du nimmst Funktionszeiger oder, falls du sie zur Verfügung hast, die Klasse std::tr1::function bzw. boost::function . Darin kannst du alle möglichen Funktionen speichern und aufrufen, die eine entsprechende Signatur haben. Aber vielleicht geht das hier auch etwas zu weit...



  • Nexus schrieb:

    In Java nimmt implementiert man dafür normalerweise Interfaces. In C++ kann man das auch machen, es gibt aber auch eine direkte Möglichkeit für Callbacks. Entweder du nimmst Funktionszeiger oder, falls du sie zur Verfügung hast, die Klasse std::tr1::function bzw. boost::function . Darin kannst du alle möglichen Funktionen speichern und aufrufen, die eine entsprechende Signatur haben. Aber vielleicht geht das hier auch etwas zu weit...

    Da ich ja so nen Java-Hintergrund habe hab ich bisher in meinem Design oft etwas Ähnliches gemacht: Abstrakte Oberklasse (pure virtual, definiert so zu sagen das Interface) und abgeleitete, implementierende Klassen. Die Referenzen bzw. Zeiger des Objektes der implementierenden Klasse "RessourcenObserver" würde ich dann als Observer bei den Spielerressourcen einfügen, sie kennt dann auch alle Buttons und kann sie de-/aktivieren. Die Buttons kann ich dann mit Kosten versehen, die gg. die Ressourcen abgeglichen würden.
    Allerdings bin ich mir über einen generischen Mechnismus bzgl. der observierten Klassen noch nicht klar, die sich bzw. ihre obervierten Zustände ja an die Observer geben müssten. In diesem Falle würde der RessourcenObserver den aktuellen Ressourcenstand der Ressourcenklasse benötigen, um ihn mit den Kosten der einzelnen Buttons abgleichen zu können. Dies ist aber die spezifische Formulierung des allgemeinen Prinzips.
    Wie wäre denn die Idee in so einem Falle mit Funktionspointern (die kenne ich auch)? Die Ressourcenklasse würde die entsprechende Funktion des Observers aufrufen und den Ressourcenstand als Parameter mitgeben? Wäre dann auch eine sehr spezifische Schnittstelle und für eine andere zu observierende Klasse müsste dann ein anderer Funktionspointer genutzt werden. Oder gibt es noch eine algemeinere Möglichkeit?



  • Mit Laufzeitpolymorphie (wie in Java) könnte das Observer-Pattern etwa folgendermassen umgesetzt werden:

    class ResourceManager // besseren Namen wählen
    {
        public:
            ~ResourceManager() // Destruktor zum Aufräumen
            {
                for (/* alle itr in myObservers */)
                   delete *itr;
            }
    
            void AddObserver(Observer* obs)
            {
                myObservers.push_back(obs);
            }
    
            void Notify() const
            {
                for (/* alle itr in myObservers */)
                   itr->Update(myCurrentResources);
            }
    
        private:
            // Kopierkonstruktor und Zuweisungsoperator verbieten
    
            std::vector<Observer*> myObservers;
            ResourceAmount         myCurrentResources;
    };
    
    class Observer
    {
        public:
            virtual ~Observer();
            virtual void Update(const ResourceAmount& res) = 0;
    };
    
    class ButtonObserver : public Observer
    {
        public:
            ButtonObserver(Button& button)
            : myButton(button)
            {
            }
    
            virtual void Update(const ResourceAmount& res)
            {
                if (EnoughResources(res))
                    myButton.Enable();
                else
                    myButton.Disable();
            }
    
        private:
            Button& myButton;
    };
    
    int main()
    {
        ResourceManager mgr;
        Button button2;
        mgr.AddObserver(new ButtonObserver(button2));
        mgr.Notify();
    }
    

    Das grössere Problem bei diesem Ansatz ist, dass du an eine Klassenhierarchie und eine Funktion mit genau festgelegter Signatur gebunden bist. Das kleinere, dass du etwas mehr Code als bei den Alternativen schreibst. Der Ansatz über Funktionszeiger:

    class ResourceManager
    {
        public:
            // Observer ist nun ein Funktionszeiger
            typedef void (*Observer)(const ResourceAmount&);
    
            void AddObserver(Observer obs);
    
            void Notify() const
            {
                for (/* alle itr in myObservers */)
                   (*itr)(myCurrentResources); // Funktionsaufruf
            }
    };
    
    void ObserveButton(const ResourceAmount& res)
    {
        // Wo kriegen wir den Button her? Zusätzlicher Parameter geht nicht,
        // weil wir in Notify() nichts übergeben können. Ausserdem hat man dann
        // keine einheitliche Schnittstelle mehr.
        // Einzige Möglichkeit: Globaler Button. Das kanns aber nicht sein.
    }
    

    Man könnte jetzt einen Memberfunktionszeiger nehmen und ihn auf eine Update() -Funktion zeigen lassen. Damit hat man im Prinzip virtuelle Funktionen nachgebaut. Allerdings geht das viel eleganter, nämlich mit einer modernen Abstraktion eines Funktionszeigers:

    class ResourceManager
    {
        public:
            // Observer ist nun irgendein Callable (aufrufbares Objekt)
            // mit kompatibler (muss nicht exakt stimmen!) Signatur
            typedef std::tr1::function<void(const ResourceAmount&)> Observer;
    
            void AddObserver(const Observer& obs);
            void Notify(); // genau wie beim zweiten Ansatz
    };
    
    // 1. Möglichkeit: Funktor, also Klasse mit operator()
    // ähnlich wie ganz oben, nur mit statischer Polymorphie
    class ButtonObserver
    {
        public:
            ButtonObserver(Button& button);
    
            // Statt Update() überladener operator()
            void operator() (const ResourceAmount& res)
            {
                // konfiguriere myButton
            }
    
        private:
            Button& myButton;
    };
    
    // 2. Möglichkeit: Freie Funktion
    void ObserveButton(ResourceAmount res, Button& button)
    {
        // Konfiguriere button
    }
    
    // 3. Möglichkeit: Memberfunktion, z.B. direkt von Button
    // hier wahrscheinlich unangebracht, aber fürs Prinzip
    class Button
    {
        public:
            void AdaptToResources(const ResourceAmount& res);
    };
    
    int main()
    {
        ResourceManager mgr;
        Button button2;
    
        // 1. Funktionsobjekt/Funktor mit operator()
        mgr.AddObserver( ButtonObserver(button2) );
    
        // 2. Freie Funktion, an Objekt gebunden
        // Erzeugt einen Funktor mit Signatur void(const ResourceAmount&)
        // Also wie 1., nur generisch und mit viel weniger Code. ObserveButton()
        // nimmt den ersten Parameter per Value statt Const-Referenz, das stellt
        // aber kein Problem dar -> Signatur muss nur kompatibel sein!
        mgr.AddObserver( tr1::bind(&ObserveButton, _1, button2) );
    
        // 3. Memberfunktion, an Objekt gebunden
        // Es wird ebenfalls ein neuer Funktor erzeugt, der direkt auf button2 
        // die Methode Button::AdaptToResources() aufruft.
        mgr.AddObserver( tr1::bind(&Button::AdaptToResources, &button2) );
    
        // Cool, oder? Ruft alles Mögliche auf.
        mgr.Notify();
    }
    

    So, ich habe dir nun ein paar moderne Möglichkeiten in C++ gezeigt. Wahrscheinlich ist es für dich momentan am einfachsten, du bleibst bei dynamischer Polymorphie. Aber früher oder später solltest du dich mit den Alternativen vertraut machen, dann könntest du z.B. hier nachschauen. 😉



  • Also was ich bisher in ähnlichen Konstellationen programmiert habe ist ähnlich Deinem ersten Vorschlag. Wobei meines Erachtens die abstrakte Klasse Observer ResourceObserver heißen müsste, da sie keine allgemeine Observerklasse ist, sondern speziell für Ressourcen zuständig.

    Letzteres war eher das, was ich im Sinn hatte: Eine generische Observerklasse, von der dann spezielle Implementierende Klassen ableiten. Wobei das mit der "normalen" Klassenhierarchie wie in Java schwierig wird, da man ja an eine einheitliche Methodensignatur gebunden ist, die durch die allgemeine Observerklasse vorgegeben wird. Für einen ResourceObserver, der von solch einer allgemeinen Klasse ableitet müssten dann die Ressourcen, gegen die geprüft werden soll auf einem anderen Weg mitgegeben werden. Dies bringt andere Einschränkungen mit sich.

    Das mit der Abstraktion des Funktionszeigers hab ich nur zum Teil verstanden. Ich nehm mal an, dass die Grundalgen wieder in der

    std::tr1::function<>
    

    zu finden sind?
    V.a. den Bind der 2. und 3. Möglichkeit hab ich nicht verstanden. Bei der 2. Möglichkeit wird der Funktor an ?? gebunden, _1 ist dort sicher ein Platzhalter, der sich auf irgend ein erstes Argument bezieht. das den "Typ" ResourceAmount hat. Aber woher kommt dieses? Das zweite Argument ist dann ganz normal der Button, entsprechend der Signatur der freien Funktion.
    In der 3. Möglichkeit wird die Memberfunktion AdaptToResources der Buttonklasse an ?? gebunden. Gerufen wird sie auf der Instanz von button2, auf den eine Referenz übergeben wird. Soweit mal meine Interpretation.

    Aber wie kann man denn in den für mich "komplizierten" Möglichkeiten des abstrakten Funktionszeigers (weil ich das Konzept noch nicht kannte) den generischen Ansatz eines "allgemeinen Observers" fahren, von dem man dann spezielle Ausprägungen (für Resourcen und anderem) machen kann? Oder geht das in dem Sinne gar nicht?



  • Reth schrieb:

    Das mit der Abstraktion des Funktionszeigers hab ich nur zum Teil verstanden. Ich nehm mal an, dass die Grundalgen wieder in der

    std::tr1::function<>
    

    zu finden sind?

    Ja. Eine Einführung in std::tr1::function bzw. boost::function (die sind eigentlich gleich) findest du beispielsweise hier.

    Reth schrieb:

    _1 ist dort sicher ein Platzhalter, der sich auf irgend ein erstes Argument bezieht. das den "Typ" ResourceAmount hat. Aber woher kommt dieses? Das zweite Argument ist dann ganz normal der Button, entsprechend der Signatur der freien Funktion.

    Exakt. Du kannst dir Platzhalter so vorstellen, dass sie die finalen Parameter des erzeugten Objekts repräsentieren. Wenn man mit std::tr1::bind einen Ausdruck ohne Platzhalter erzeugt, ist der resultierende Funktor parameterlos, da bereits alle ursprünglichen Parameter mit konkreten Werten gefüllt wurden. std::tr1::bind ist also ein Hosentaschen-Fabrik, mit der man flexibel Funktoren zusammenbauen kann.

    In unserem Beispiel nehmen wir einen Zeiger auf die ObserveButton -Funktion:

    tr1::bind(&ObserveButton, _1, button2)
    

    Beim Aufruf wird als erstes Argument der Platzhalter _1 eingesetzt (d.h. noch kein konkreter Wert). Das zweite Argument wäre der feststehende button2 . Generiert wird also sowas:

    struct Functor
    {
        Functor(Button& static_arg2)
        : arg2(static_arg2)
        {
        }
    
        void operator() (const ResourceAmount& arg1)
        {
            ObserveButton(arg1, arg2);
        }
    
        Button& arg2;
    };
    
    // schlussendlich (mit ein paar Kopien dazwischen):
    Functor functorInstance(button2);
    

    Der entstehende Funktor wird nachher so im ResourceManager gespeichert. Beim Aufruf wird die Membervariable myResources für den Platzhalter _1 eingesetzt:

    // Überladener operator() wird aufgerufen
    functorInstance(myResources);
    

    Reth schrieb:

    In der 3. Möglichkeit wird die Memberfunktion AdaptToResources der Buttonklasse an ?? gebunden. Gerufen wird sie auf der Instanz von button2, auf den eine Referenz übergeben wird. Soweit mal meine Interpretation.

    Bei Memberfunktionen ist das Prinzip das gleiche, nur dass der erste bind() -Parameter (nach der Angabe der Funktion) immer für den this -Zeiger des Objekts steht, auf welchem die Methode aufgerufen wird. Grundsätzlich kannst du dir eine Memberfunktion auch als freie Funktion vorstellen:

    void MyClass::Member(int a, double b);
    void MyClass_Member(MyClass* this, int a, double b);
    

    Reth schrieb:

    Aber wie kann man denn in den für mich "komplizierten" Möglichkeiten des abstrakten Funktionszeigers (weil ich das Konzept noch nicht kannte) den generischen Ansatz eines "allgemeinen Observers" fahren, von dem man dann spezielle Ausprägungen (für Resourcen und anderem) machen kann? Oder geht das in dem Sinne gar nicht?

    Auf der Observerseite hast du im Grunde nur noch verpackte Funktionen ( std::tr1::function ), da kannst du nicht mehr viel generischer werden. Auf der Registrierungsklassen-Seite hingegen könntest du Templates verwenden.

    template <typename ObserverArgument>
    class RegistrationCenter
    {
        public:
            typedef std::tr1::function<void(ObserverArgument)> Observer;
    
            void Register(const Observer& obs) // hiess vorher AddObserver()
            {
                myObservers.push_back(obs);
            }
    
            void Notify(ObserverArgument arg) const
            {
                for (/* alle itr in myObservers */)
                   (*itr)(arg); // Funktionsaufruf
            }
    
        private:
            std::vector<Observer> myObservers;
    };
    

    Auf das Ressourcenbeispiel angewandt könnte eine Instanziierung so aussehen:

    RegistrationCenter<const ResourceAmount&> mgr;
    mgr.Register(/* irgendeine Funktion */);
    // ...
    mgr.Notify(myResourceAmount);
    

    Das könnte man aus einer anderen Klasse heraus tun (z.B. wäre dann myResourceAmount eine Membervariable). Kann aber auch sein, dass ich dich komplett falsch verstanden habe, in dem Falle müsstest du vielleicht etwas konkreter werden.



  • Hi nochmal!

    Danke! Genau so, wie in Deinem letzten Bsp. hab ich mir das vorgestellt! Allerdings sieht mein momentaner Code so aus, dass noch viele Konzepte durcheinander angewandt werden. Z.B. nehm ich noch nicht überall das Observer-Pattern, wo es angebracht wäre. Z.T. übergebe ich Referenzen/Zeiger auf Objekte, die geändert werden müssten direkt an die ändernden Klassen (z.B. wird das Cursorobjekt einmal an die Buttons übergeben, bei deren Aktivierung es sein Aussehen ändern soll und es wird zu dem an das Objekt [Observer] übergeben, welches auf Mausbewegungen hört, damit es an der Mausposition angezeigt werden kann). Gibt es da eigentlich eine gute Daumenregel, ab wann man sich besser an einheitliche Konzepte hält und wann man den pragmatischen Ansatz wählt (abgesehen von der persönlichen Präferenz natürlich)?



  • Ich würde mir da nicht allzu grosse Sorgen machen. Ältere Projekte von mir verwenden kaum Design Patterns (zumindest nicht bewusst), aber ich würde jetzt nicht sagen, dass der Code deswegen nicht durchdacht ist.

    Es ist sicher nicht schlecht, ab und zu neue Entwurfsmuster auszuprobieren, weil sie teilweise wirklich elegante Möglichkeiten mit sich bringen. Aber den Code auf Biegen und Brechen so zu gestalten, dass man möglichst viele davon nimmt, würde ich nicht. Oft spricht auch gegen den "pragmatischen" Ansatz nichts. Ich würde mir im konkreten Fall überlegen, bei welcher Variante mehr Code nötig ist, wo mehr Abhängigkeiten zwischen Klassen bestehen, wo bessere Erweiterbarkeit und Wartbarkeit gewährleistet wird, und wie wichtig diese Faktoren für jenen Fall überhaupt sind. Implementierungsaufwand und -zeit ist natürlich auch ein Kriterium, wobei das in Privatprojekten weniger ins Gewicht fällt. Man hat dafür mehr Zeit zum Experimentieren... 😉



  • Pattern dienen ja auch dazu einen Denkansatz zu bekommen. Oftmals verwende ich am Ende nicht wirklich das Pattern, sondern etwas ähnliches, was ein paar Gedanken des Pattern vereint, aber einfach ein Pattern abzubilden, damit man es benutzt macht nicht viel Sinn. Z.B das MVC Pattern benutzt ich in einer abgeänderter Version, aber die Idee dahinter bemerkt man ganz klar.



  • drakon schrieb:

    Oftmals verwende ich am Ende nicht wirklich das Pattern, sondern etwas ähnliches, was ein paar Gedanken des Pattern vereint

    Deshalb heißen die Dinger auch Pattern, weils eben keine feste Schablone ist sondern nur eine grobe Richtung. Ein Pattern genau nach Schema F wies im Buch steht abzubilden ist nicht Sinn der Sache.



  • Danke für eure Tips. Hatte mich wohl etwas missverständlich ausgedrückt. Natürlich geht es mir nicht um das sklavische Befolgen von irgendwelchen Vorgaben (Pattern o.ä.). Mir ist eher wichtiger zu wissen, ab wann ein Refactoring angesagt ist (quasi die "Schmerzgrenze" bzw. der Schwellwert), falls es dafür Kriterien gibt.

    Mit den Anregungen aus dem Thread (inhaltliche und technische) hab ich jedenfalls schon ein paar gute Ideen für demnächst!



  • pumuckl schrieb:

    drakon schrieb:

    Oftmals verwende ich am Ende nicht wirklich das Pattern, sondern etwas ähnliches, was ein paar Gedanken des Pattern vereint

    Deshalb heißen die Dinger auch Pattern, weils eben keine feste Schablone ist sondern nur eine grobe Richtung. Ein Pattern genau nach Schema F wies im Buch steht abzubilden ist nicht Sinn der Sache.

    Ja, das ist klar, aber ich meinte noch einen Schritt weiter. So dass man nicht sagen kann das ist jetzt das Pattern, sondern das benutzt vielleicht lediglich eine Idee davon.


Anmelden zum Antworten