Design-Problem: Composite ohne friend



  • Guten Abend,

    Bei meinem kleinen GUI-Framework habe ich folgendes Design (Composite-Pattern):

    // Basisklasse für GUI-Komponenten
    class Component
    {
        public:
            virtual void HandleEvent(const Event& Ev) = 0;
    };
    
    // Klasse für elementare Komponenten wie Schaltflächen, Textfelder etc.
    class Widget : public Component
    {
        public:
            virtual void HandleEvent(const Event& Ev)
            {
                // reagiere hier auf Event
            }
    };
    
    // Klasse für Container-Komponenten, die andere Komponenten enthalten können
    class Panel : public Component
    {
        public:
            virtual void HandleEvent(const Event& Ev)
            {
                // reagiere für alle Subkomponenten auf Events:
                for (/* Itr in MySubComponents */)
                    (*Itr)->HandleEvent(Ev);
            }
    
        private:
            std::vector<Component*> MySubComponents;
    };
    

    So weit, so schlecht. Das Problem ist nämlich, dass HandleEvent() öffentlich ist, aber grundsätzlich soll der User diese Funktion nicht direkt aufrufen können. Ich würde sie sehr gerne protected machen, doch C++ lässt Zugriffe auf protected -Member nicht zu, wenn sie nicht zu this gehören. Somit könnte ich (*Itr)->HandleEvent() nicht aufrufen.

    Das einzige, was mir dazu einfällt, wäre ein friend class Panel in der Basisklasse Component . Aber schön finde ich das nicht, Component soll schliesslich nichts von Panel wissen.

    Sieht jemand hier gerade eine Möglichkeit, ohne friend auszukommen?


  • Administrator

    // Basisklasse für GUI-Komponenten
    class Component
    {
    protected:
        static void ForwardEvent(Component* comp, Event const& Ev)
        {
            comp->HandleEvent(Ev);
        }
    
    private:
        virtual void HandleEvent(const Event& Ev) = 0;
    };
    
    // Klasse für elementare Komponenten wie Schaltflächen, Textfelder etc.
    class Widget : public Component
    {
    private:
        virtual void HandleEvent(const Event& Ev)
        {
            // reagiere hier auf Event
        }
    };
    
    // Klasse für Container-Komponenten, die andere Komponenten enthalten können
    class Panel : public Component
    {
    private:
        virtual void HandleEvent(const Event& Ev)
        {
            // reagiere für alle Subkomponenten auf Events:
            for(int i = 0; i < MySubComponents.size(); ++i)
            {
                Component::ForwardEvent(MySubComponents[i], Ev);
            }
        }
    
    private:
        std::vector<Component*> MySubComponents;
    };
    

    Grüssli



  • Hey, sehr schön. Vielen Dank für die schnelle Hilfe! 🙂

    Ich habe die Funktionen in den Klassen Widget und Panel per using in den privaten Bereich gezogen, sodass sie von abgeleiteten Klassen nicht mehr aufgerufen werden können. Statische Funktionen als Standard-Workaround gegen die protected -Regel muss ich mir übrigens merken.

    Falls sonst noch jemand etwas weiss oder grundlegende Designvorschläge hat, nur her damit. 😉


  • Administrator

    Nexus schrieb:

    Ich habe die Funktionen in den Klassen Widget und Panel per using in den privaten Bereich gezogen, sodass sie von abgeleiteten Klassen nicht mehr aufgerufen werden können.

    Welche Funktionen? In meinem Beispiel ist ja alles private , ausser ForwardEvent . Was willst du dann noch mehr private machen? 🙂

    Grüssli



  • Dravere schrieb:

    Welche Funktionen? In meinem Beispiel ist ja alles private , ausser ForwardEvent . Was willst du dann noch mehr private machen? 🙂

    Ah, sorry. Bei mir betrifft das eben nicht nur das Event-Handling, sondern z.B. auch das Zeichnen, was ich im oberen Codebeispiel der Einfachheit unterschlagen habe.

    Bezogen auf das Beispiel hier habe ich ein using Component::ForwardEvent; im private -Bereich der Klassen Panel und Widget .


  • Administrator

    Nexus schrieb:

    Bezogen auf das Beispiel hier habe ich ein using Component::ForwardEvent; im private -Bereich der Klassen Panel und Widget .

    Das bringt dir einen alten Hut. Component::ForwardEvent kann man dann trotzdem noch aufrufen von einer abgeleiteten Klasse weiter unten in der Hierarchie. Also:

    class Panel : public Component
    {
    private:
      using Component::ForwarEvent;
    };
    
    class DerivedPanel : public Panel
    {
    private:
      virtual void HandleEvent(Event const& Ev);
      {
        Component::ForwareEvent(this, Ev); // keine Problem!
      }
    };
    

    Oder meinst du etwas anderes? 😕

    Grüssli


  • Mod

    Component::ForwardEvent muss hier nicht statisch sein, es ist nur zweckmäßig.
    Den Zugriff auf eine einzige abgeleitete Klasse zu beschränken, geht nur per freind, denn friend-Beziehungen sind im Gegensatz zu Vererbungsbeziehungen nicht transitiv.

    class ComponentBase
    {
    private:
        virtual void HandleEvent(const Event& Ev) = 0;
        friend class Panel;
    };
    
    class Component : protected ComponentBase
    {
    ...
    };
    
    class Widget : public Component
    {
    private:
        virtual void HandleEvent(const Event& Ev)
        {
            // reagiere hier auf Event
        }
    };
    
    class Panel : public Component
    {
    private:
        virtual void HandleEvent(const Event& Ev)
        {
            // reagiere für alle Subkomponenten auf Events:
            for (/* Itr in MySubComponents */)
                (*Itr)->HandleEvent(Ev);
        }
    
    private:
        std::vector<Component*> MySubComponents;
    };
    


  • @ Dravere:
    Ja, schon so. Dass es grundsätzlich noch möglich ist, ist mir bewusst (gerade gestern habe ich dazu was geschrieben). Aber man kann nur noch über die Indirektion Component:: die Basisklassenversion aufrufen, was immerhin schon einige Fehler abfangen dürfte. Die Funktion dennoch aufzurufen würde ich fast schon als böswillig einstufen.

    Oder meinst du, ich sollte auf das using verzichten?



  • @ camper:
    Hm. Ich versuche friend eigentlich zu umgehen, allerdings nehme ich dafür zwei zusätzliche Indirektionsfunktionen (momentan für Events und Zeichnen, möglicherweise kommen noch mehr dazu) und die Gefahr, von abgeleiteten Klassen immer noch Zugriff zu haben, in Kauf (wobei letzteres wie angetönt vertretbar sein sollte).

    Ich verwende eben schon sonst ab und zu friend und möchte nicht dazu tendieren, wegen der verlockenden Einfachheit unüberlegte Entscheidungen zu treffen. Wenn allerdings die Alternative mehr Aufwand und mehr Nachteile mit sich bringt, ist der Verzicht vielleicht schon etwas fragwürdig...


  • Administrator

    Nexus schrieb:

    Oder meinst du, ich sollte auf das using verzichten?

    *schulterzuck*
    Ich glaube darüber könnte man sich endlos streiten, ob das sinnvoll ist oder nicht. Somit bin ich der Meinung, dass das eine sehr subjektive Sache ist. Ich würde vielleicht den Namen ein wenig anders wählen: forwardEventTo(Event const&, Component& cmp). Man könnte sogar noch ein static davor setzen oder sowas. Ich würde probieren den Schutz davor, dass man diese Funktion nicht absichtlich aufruft, eher in Component einbauen, damit gleich jeder, welcher von Component ableitet, von diesem Schutz profitiert. Allerdings ist schon fragwürdig, ob es überhaupt so einen Schutz braucht?

    Zur Lösung von camper:
    Es sind zwei ganz unterschiedliche Ziele, welche da erreicht werden, die musst du unterscheiden Nexus. Die Lösung von camper erlaubt wirklich nur der Klasse Panel den Zugriff. Wenn nun ein Programmierer kommt, welcher deine Bibliothek verwendet, und eine eigene spezielle Version einer Panelklasse erstellen möchte, dann hat er keine Möglichkeit dies zu machen. Du würdest ihn somit völlig in seinen Möglichkeiten behindern. Vielleicht ist es gewollt, dann ist die Lösung von camper die bessere, ansonsten würde ich eher die andere nehmen 😉

    Mit friend kann man wirklich geschlossene Funktionalität erreichen, welche niemand von aussen für eigene Zwecke nutzen kann.

    Grüssli



  • Designhinweis.

    Du solltest zwischen dem Logik (Beispielweise das ein Button klickbar ist) und wie der Benutzer dieses Implementiert unterscheiden.

    class Button
    {
    public:
      void (*Clicked)(const Event& e);
    };
    
    class MyButton : public Button
    {
    public: 
      MyButton() : Clicked(&ClickHandle) {
      }
    
    private:
      void ClickHandle(const Event& e) {
      ...
      }
    };
    


  • Dravere schrieb:

    Ich würde probieren den Schutz davor, dass man diese Funktion nicht absichtlich aufruft, eher in Component einbauen, damit gleich jeder, welcher von Component ableitet, von diesem Schutz profitiert. Allerdings ist schon fragwürdig, ob es überhaupt so einen Schutz braucht?

    Du meinst private ? Oder doch eher throw UnsupportedOperationException ? 😃

    Nein, du hast schon Recht. Aber Component ist eigentlich nicht dafür vorgesehen, vom Benutzer direkt abgeleitet zu werden (eine GUI-Komponente ist bei mir entweder elementar oder eine Sammlung anderer Komponenten). Ich muss aber sagen, dass mein Framework noch nicht sehr weit entwickelt ist, möglicherweise werde ich hier noch einiges ändern. Ein detail -Namensraum wäre vielleicht ein Weg, um zu zeigen, dass die Klasse für die Implementierung reserviert ist.

    Dravere schrieb:

    Es sind zwei ganz unterschiedliche Ziele, welche da erreicht werden, die musst du unterscheiden Nexus. Die Lösung von camper erlaubt wirklich nur der Klasse Panel den Zugriff. Wenn nun ein Programmierer kommt, welcher deine Bibliothek verwendet, und eine eigene spezielle Version einer Panelklasse erstellen möchte, dann hat er keine Möglichkeit dies zu machen.

    In der HandleEvent() -Funktion wird zuerst HandleEvent() für alle Subkomponenten aufgerufen, und dann OnEvent() für die aktuelle Instanz, welche vom Benutzer implementiert werden kann. Das Verhalten möchte ich nicht ändern, mittels OnEvent() sind ja benutzerdefinierte Event-Reaktionen in Panel -Derivaten immer noch möglich.

    Zeus schrieb:

    Du solltest zwischen dem Logik (Beispielweise das ein Button klickbar ist) und wie der Benutzer dieses Implementiert unterscheiden.

    Ja, aber ich will den Benutzer eines Button s nicht zur Vererbung zwingen. Ich verwende hier das Observer-Pattern und biete die Möglichkeit, Callbacks zu registrieren. Dann kann auch mehr als nur eine Aktion mit dem Mausklick assoziiert werden.



  • Vererbung stört? Na dann ohne:

    void ClickHandle(const Event& e)
    {
    }
    
    int main()
    {
      Button button();
      button.Clicked = &ClickHandle;
      button.Clicked() // Manuell Fire Click.
      return 0;
    }
    

    Nun die Vererbung ist nicht das stört. Was du brauchst ist ein öffentlich Schnistelle um Clicken auszulösen.



  • Zeus schrieb:

    Nun die Vererbung ist nicht das stört. Was du brauchst ist ein öffentlich Schnistelle um Clicken auszulösen.

    Wie gesagt habe ich dafür schon einen flexibleren Ansatz gefunden.

    void ClickAction()
    {
        std::cout << "Klick." << std::endl;
    }
    
    int main()
    {
        Button Btn;
        Btn.AddMouseClickListener(&ClickAction);
        Btn.Click();
        // ...
    }
    

    Öffentliche Member sind sicher keine Option.



  • Nexus schrieb:

    Zeus schrieb:

    Nun die Vererbung ist nicht das stört. Was du brauchst ist ein öffentlich Schnistelle um Clicken auszulösen.

    Wie gesagt habe ich dafür schon einen flexibleren Ansatz gefunden.

    void ClickAction()
    {
        std::cout << "Klick." << std::endl;
    }
    
    int main()
    {
        Button Btn;
        Btn.AddMouseClickListener(&ClickAction);
        Btn.Click();  // <- öffentlich Schnistelle, da ist das Teil wovon ich spreche
        // ...
    }
    


  • In deinem ersten Post hast du zwar nichts davon gesagt, aber okay. 😉

    Dass ich Mausklicks mittels Click() simulieren kann, ist ja schön und gut. Ob eine Schaltfläche diese Funktionalität unbedingt braucht, ist wieder eine andere Frage. Ich habe sie zumindest.



  • Qt, WCF haben Sie auch *gg* blos Swing nicht, da ist die Methode protected 😛



  • Zeus schrieb:

    blos Swing nicht, da ist die Methode protected 😛

    Nein, auch da ist sie public :
    http://java.sun.com/javase/6/docs/api/ und dann bei JButton nach doClick() suchen.

    Welcher Name gefällt euch eigentlich besser, "DoClick" oder "Click"? Mich stört das nichtssagende "Do" ein wenig.


  • Administrator

    Zeus schrieb:

    Qt, WCF haben Sie auch *gg* blos Swing nicht, da ist die Methode protected 😛

    Hmmm, gute Argumentation 🤡 :p

    Ich habe so eine Methode noch nie benötigt und hab sie bis anhin nur missbraucht gesehen. Statt dass man die Funktionalität in eine externe Funktion ausgelagert hat, welche im Click-Event aufgerufen wird und auch von extern aufgerufen werden kann, haben es die Leute in die Handlerfunktion geschrieben und dann per DoClick oder dergleichen die Handlerfunktion aufgerufen. Wodurch meistens noch ein unnötiges Event erzeugt werden musste.

    Ich sehe ehrlich gesagt keinen sinnvollen Verwendungszweck für so eine Funktion.

    Grüssli



  • Dravere schrieb:

    Ich sehe ehrlich gesagt keinen sinnvollen Verwendungszweck für so eine Funktion.

    Da ich intern mit std::tr1::function arbeite, wird der Anwender auch ab und zu std::tr1::bind() benutzen, um mal schnell lokale Funktoren zu übergeben. Dabei kann ich mir vorstellen, dass es angenehmer ist, später einfach Click() aufzurufen, statt zu rekonstruieren, welche Funktionen alle gespeichert wurden, diese neu mit bind() zusammenzubauen und jeweils manuell aufzurufen. Und Funktionen jeweils neu zu definieren macht bind() auch hinfällig. Denn die Sache sieht im Allgemeinen ja nicht so trivial wie im oberen Codebeispiel aus, wo man ClickAction() gerade so gut direkt aufrufen könnte. Im Extremfall wäre es notwendig, über die gespeicherten Funktionen Buch zu führen (d.h. sie doppelt zu speichern) oder auf der User-Seite einen Callback-Mechanismus einzurichten.

    Gesamthaft gesehen denke ich, man kann sich durch eine Click() -Funktion so einiges an Komplexität sparen. Natürlich kann sie auch missbraucht werden, aber das halte ich jetzt nicht gerade für das K.O.-Kriterium.


Anmelden zum Antworten