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 gerneprotected
machen, doch C++ lässt Zugriffe aufprotected
-Member nicht zu, wenn sie nicht zuthis
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 BasisklasseComponent
. Aber schön finde ich das nicht,Component
soll schliesslich nichts vonPanel
wissen.Sieht jemand hier gerade eine Möglichkeit, ohne
friend
auszukommen?
-
// 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
undPanel
perusing
in den privaten Bereich gezogen, sodass sie von abgeleiteten Klassen nicht mehr aufgerufen werden können. Statische Funktionen als Standard-Workaround gegen dieprotected
-Regel muss ich mir übrigens merken.Falls sonst noch jemand etwas weiss oder grundlegende Designvorschläge hat, nur her damit.
-
Nexus schrieb:
Ich habe die Funktionen in den Klassen
Widget
undPanel
perusing
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
, ausserForwardEvent
. Was willst du dann noch mehrprivate
machen?Grüssli
-
Dravere schrieb:
Welche Funktionen? In meinem Beispiel ist ja alles
private
, ausserForwardEvent
. Was willst du dann noch mehrprivate
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;
imprivate
-Bereich der KlassenPanel
undWidget
.
-
Nexus schrieb:
Bezogen auf das Beispiel hier habe ich ein
using Component::ForwardEvent;
imprivate
-Bereich der KlassenPanel
undWidget
.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
-
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 IndirektionComponent::
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 versuchefriend
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...
-
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 nehmenMit
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 eherthrow 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. Eindetail
-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 zuerstHandleEvent()
für alle Subkomponenten aufgerufen, und dannOnEvent()
für die aktuelle Instanz, welche vom Benutzer implementiert werden kann. Das Verhalten möchte ich nicht ändern, mittelsOnEvent()
sind ja benutzerdefinierte Event-Reaktionen inPanel
-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 beiJButton
nachdoClick()
suchen.Welcher Name gefällt euch eigentlich besser, "DoClick" oder "Click"? Mich stört das nichtssagende "Do" ein wenig.
-
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 zustd::tr1::bind()
benutzen, um mal schnell lokale Funktoren zu übergeben. Dabei kann ich mir vorstellen, dass es angenehmer ist, später einfachClick()
aufzurufen, statt zu rekonstruieren, welche Funktionen alle gespeichert wurden, diese neu mitbind()
zusammenzubauen und jeweils manuell aufzurufen. Und Funktionen jeweils neu zu definieren machtbind()
auch hinfällig. Denn die Sache sieht im Allgemeinen ja nicht so trivial wie im oberen Codebeispiel aus, wo manClickAction()
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.