Kleinere Probleme beim Ausprobieren des Bridge-Patterns
-
philipp2100 schrieb:
Genau das geht eben nicht, da dein
GtkWindowImpljaprivatevonGtk::Windowerbt. Außerdem brauchst du zum Upcasten keinen expliziten Cast, das geht auch implizit.Mhh ok, ich hatte das gestern Abend nur kurz durch den Compiler gejagt und da gabs zumindest keine Typfehler mehr. Auf die Gefahr hin mich weiter als totaler Anfänger zu outen, könntest du kurz erklären, warum mein Ansatz nicht funktioniert? (Ich lese bei dir heraus, dass das an der private Vererbung liegt?)
philipp2100 schrieb:
Ohne jetzt anmaßend sein zu wollen, aber wie wärs denn, wenn du erst mal ein schönes GUI-Programm mit einem netten Toolkit programmierst? ... Es ist so schon herausfordernd, da sollte man wenigstens genau wissen, wie das zu designende System genau arbeitet.
Ich bin ja für jede Hilfe dankbar und denke auch das du Recht hast. Bin da momentan wohl etwas zu überambitioniert. Dennoch bleibt für mich weiterhin (also ohne den ganzen Schnickschnack bzgl. austauschbare Toolkits, etc.) die Frage, wie man nun genau an das Erstellen einer Gui für ein Programm rangeht (im Sinne von Klassenaufbau, welche braucht man, was haben die typischerweise für Methoden, etc.). Am liebsten wäre mir quasi mal den Beispielcode von einem gut strukturierten (aber kleinem) Programm zu sehn, um ein realitätsnahes Bsp. diesbezüglich zu haben. Ansonsten wird ich wohl erstmal deinem Vorschlag entsprechen und besser klein anfangen.
-
Wenn du über dein Klassendesign nachdenken musst machst du es falsch. "Ich habe eine Anwendungslogik, ich habe ein Ding was Fenster macht, jetzt schreibe ich eine Klasse, die eine Anwendungslogik und ein Fenster-Ding hat und stelle die Anwendungslogik im Fenster dar."
Keine Vererbung, keine Interfaces, keine Casts. Einfach nur simpel hinschreiben was passieren soll. Damit kannst du das Fenster-Toolkit wechseln ohne die Anwendungslogik anzufassen und umgekehrt. Generischer und einfacher wird es nicht. Ich halte es langsam für einen Fehler Leuten überhaupt Vererbung und seltsame Design-Patterns beizubringen.
-
Meiner schrieb:
könntest du kurz erklären, warum mein Ansatz nicht funktioniert? (Ich lese bei dir heraus, dass das an der private Vererbung liegt?)
Ja genau, denn wenn du
privateerbst, hat die erbende Klassen die ganzen geerbten Methoden nach außen hin ja nicht. Dementsprechend wäre es sinnfrei, ein wie auch immer geartetes Casten zuzulassen.Meiner schrieb:
Am liebsten wäre mir quasi mal den Beispielcode von einem gut strukturierten (aber kleinem) Programm zu sehn, um ein realitätsnahes Bsp. diesbezüglich zu haben.
Ich kanns mal versuchen:
enum Toolkit { TK_GTK, TK_WXWIDGETS, TK_QT }; template <int toolkit> class Window { public: virtual void show() = 0; }; template <> class Window<TK_GTK> : protected Gtk::Window { public: virtual void show() { Gtk::Window::show(); } }; template <> class Window<TK_WXWIDGETS> { wxFrame* frame; public: virtual void show() { frame->show(); } };In dem Beispiel gehe ich davon aus, dass sich ein wxFrame besser als Member handlen lässt. Für das
gtk::Windownehme ichprotected-Vererbung, damit man auch in abgeleiteten Klassen weiter Zugriff darauf hat - das oben angesprochene Problem tritt aber wie auch bei derprivate-Vererbung auf. So würdest du halt für jedes Toolkit so Spezialisierungen von deinen generischen Klassentemplates machen.
-
nwp3 schrieb:
Wenn du über dein Klassendesign nachdenken musst machst du es falsch.
Sorry, aber ich hab praktisch noch nie einen dämlicheren Spruch über Softwaredesign gehört. Design ist mit das Kritischste überhaupt für ein qualitativ hochwertiges größeres Projekt. C++ ist ja extrem reich an Ausdrucksmöglichkeiten und dadurch eine sehr komplexe Sprache. Zu behaupten, einem Anfänger oder Fortgeschrittenem in der Sprache (oder auch einem Profi) würde das Design eines kompletten Toolkits einfach so zufliegen, ist wirklichkeitsfremd.
nwp3 schrieb:
"Ich habe eine Anwendungslogik, ich habe ein Ding was Fenster macht, jetzt schreibe ich eine Klasse, die eine Anwendungslogik und ein Fenster-Ding hat und stelle die Anwendungslogik im Fenster dar." [...] Generischer und einfacher wird es nicht.
Ganz so einfach ist es eben nicht, ein Fenster hat halt auch noch Elemente etc., die u.U. in jedem Toolkit völlig anders programmiert werden müssen. Ich denke, du hast das Problem nicht verstanden.
-
philipp2100 schrieb:
nwp3 schrieb:
Wenn du über dein Klassendesign nachdenken musst machst du es falsch.
Sorry, aber ich hab praktisch noch nie einen dämlicheren Spruch über Softwaredesign gehört. Design ist mit das Kritischste überhaupt für ein qualitativ hochwertiges größeres Projekt. C++ ist ja extrem reich an Ausdrucksmöglichkeiten und dadurch eine sehr komplexe Sprache. Zu behaupten, einem Anfänger oder Fortgeschrittenem in der Sprache (oder auch einem Profi) würde das Design eines kompletten Toolkits einfach so zufliegen, ist wirklichkeitsfremd.
Ich will nicht sagen, dass es einfach ist sich ein kompliziertes Klassendesign zu überlegen. Ich will sagen, dass du statt dir ein kompliziertes Klassendesign zu überlegen lieber die Realität ansehen solltest. Public Vererbung bedeutet "ist ein". Private Vererbung heißt "Ist implementiert mithilfe von". Eine Membervariable bedeutet "hat ein". Und jetzt vergiss alles was du über Klassendesign weißt und schreibe auf was dein Programm so tut. Automatisch ergibt sich das Klassendesign indem du "Hat ein", "Ist ein" und "Ist implementiert mit" 1 zu 1 in C++ übersetzt.
Ich befürchte, dass du mit deiner Klassendesignerei völlig an der Realität vorbei ziehst und viele unnötige Probleme erschaffst, die daher rühren, dass dein kompliziertes Klassenmodell und das Problem auseinander driften. Mir ist auch unklar wie diese Realität aussieht, die man mithilfe von deinen benutzten Ausdrucksmitteln beschreibt. Du solltest diese komplizierten Ausdrucksmöglichkeiten nur nutzen wenn du musst, nicht wenn du kannst.philipp2100 schrieb:
nwp3 schrieb:
"Ich habe eine Anwendungslogik, ich habe ein Ding was Fenster macht, jetzt schreibe ich eine Klasse, die eine Anwendungslogik und ein Fenster-Ding hat und stelle die Anwendungslogik im Fenster dar." [...] Generischer und einfacher wird es nicht.
Ganz so einfach ist es eben nicht, ein Fenster hat halt auch noch Elemente etc., die u.U. in jedem Toolkit völlig anders programmiert werden müssen. Ich denke, du hast das Problem nicht verstanden.
Ich sehe den Widerspruch nicht. Ein Toolkit ist ein Ding was den Zustand der Logik darstellen kann. Das kann man mit einer einfachen Funktion ausdrücken: void Toolkit::display(const Anwendungslogik &al); Der Rest des Toolkits ist private und kümmert sich darum wie man GTK/WinAPI/QT dazu bringt die Anwendungslogik darzustellen. Ich verstehe nicht warum du dafür mit virtuellen Funktionen, Pimpl und sonstigem Gedöns rummachst.
-
nwp3 schrieb:
Ich will nicht sagen, dass es einfach ist sich ein kompliziertes Klassendesign zu überlegen. Ich will sagen, dass du statt dir ein kompliziertes Klassendesign zu überlegen lieber die Realität ansehen solltest. Public Vererbung bedeutet "ist ein". Private Vererbung heißt "Ist implementiert mithilfe von". Eine Membervariable bedeutet "hat ein". Und jetzt vergiss alles was du über Klassendesign weißt und schreibe auf was dein Programm so tut. Automatisch ergibt sich das Klassendesign indem du "Hat ein", "Ist ein" und "Ist implementiert mit" 1 zu 1 in C++ übersetzt.
Ganz so einfach ist es eben nicht mehr, wenn der Umfang mal größer wird. Da gibt es dann eben Konzepte wie "verwaltet Speicher für", "bindet ein", "ist Handle für", "ist Interface-Klasse für" etc., die entsprechend komplexer modelliert werden müssen.
nwp3 schrieb:
Das kann man mit einer einfachen Funktion ausdrücken: void Toolkit::display(const Anwendungslogik &al); Der Rest des Toolkits ist private und kümmert sich darum wie man GTK/WinAPI/QT dazu bringt die Anwendungslogik darzustellen. Ich verstehe nicht warum du dafür mit virtuellen Funktionen, Pimpl und sonstigem Gedöns rummachst.
Und wie willst du die
display()-Funktion schreiben, ohne den Rest des Programms zu kennen?edit: Damit du mich nicht falsch verstehst: Im Grunde bin ich ganz bei dir und sehe auch ein großes Problem in der Verwendung unnötig komplizierter Designlogiken. Da muss man schon drauf achten, aber ich würde das jetzt nicht als unnötig kompliziert erachten. Wenn du eine bessere Idee hast dann poste!
-
philipp2100 schrieb:
nwp3 schrieb:
Das kann man mit einer einfachen Funktion ausdrücken: void Toolkit::display(const Anwendungslogik &al); Der Rest des Toolkits ist private und kümmert sich darum wie man GTK/WinAPI/QT dazu bringt die Anwendungslogik darzustellen. Ich verstehe nicht warum du dafür mit virtuellen Funktionen, Pimpl und sonstigem Gedöns rummachst.
Und wie willst du die
display()-Funktion schreiben, ohne den Rest des Programms zu kennen?Die display-Funktion hat doch alles. Sie hat über den Parameter Zugriff auf den öffentlichen Zustand der Programmlogik und über die Toolkit::-Zugehörigkeit Zugriff auf sämtliche Fensterfunktionen. Was fehlt denn?
philipp2100 schrieb:
Ganz so einfach ist es eben nicht mehr, wenn der Umfang mal größer wird. Da gibt es dann eben Konzepte wie "verwaltet Speicher für", "bindet ein", "ist Handle für", "ist Interface-Klasse für" etc., die entsprechend komplexer modelliert werden müssen.
Und hier liegt dein Denkfehler. "verwaltet Speicher für" gibt es in so einer Anwendung gar nicht. Die allermeisten Anwendungen interessieren sich nicht für Speicher. Das ist ein Implementationsdetail und muss als solches behandelt werden. "bindet ein" verstehe ich nicht bzw. kann mit verschiedene Dinge drunter vorstellen. "ist Interface-Klasse für" existiert außerhalb von Java-Interpretern u.ä. nicht und ist damit nicht Teil des Klassendesigns. Dein Klassendesign darf nur Dinge enthalten, die von der Anwendung gefordert sind und schon die Existenz einer Klasse wird praktisch nie gefordert. Nachdem du modelliert hast was die Anwendung tun soll kannst du dich in der Implementation mit dem Bridge-Pattern beschäftigen wenn du gerne möchtest. Aber dann ist alles so weit abstrahiert und gekapselt, dass es nur relativ kleine Module sind, die sowas hoffentlich nicht erfordern.
-
nwp3 schrieb:
Die display-Funktion hat doch alles. Sie hat über den Parameter Zugriff auf den öffentlichen Zustand der Programmlogik und über die Toolkit::-Zugehörigkeit Zugriff auf sämtliche Fensterfunktionen. Was fehlt denn?
Eine generische display()-Funktion, die Teil eines Toolkits ist, das VOR dem eigentlichen Programm entworfen wird, kann doch gar nichts von der Programmlogik wissen, die sie darstellen soll.
nwp3 schrieb:
"verwaltet Speicher für" gibt es in so einer Anwendung gar nicht.
Doch, sogar in jeder größeren gibt es eine Speicherverwaltung, die selbstverständlich auch objektorientiert designt werden kann.
nwp3 schrieb:
Die allermeisten Anwendungen interessieren sich nicht für Speicher. Das ist ein Implementationsdetail und muss als solches behandelt werden.
Was dich aber nicht davon entbindet, es irgendwie und irgendwo zu implementieren.
nwp3 schrieb:
"bindet ein" verstehe ich nicht bzw. kann mit verschiedene Dinge drunter vorstellen.
Könnte z.B. eine Klasse einer Pluginschnittstelle sein, die Plugins verwaltet.
nwp3 schrieb:
"ist Interface-Klasse für" existiert außerhalb von Java-Interpretern u.ä. nicht und ist damit nicht Teil des Klassendesigns.
Guck mal in den Stroustrup. Dort wird der Begriff (auch ziemlich naheliegend) wie ich finde für Klassen verwendet, die ein anderes (einfacheres oder vielseitigeres) Interface für andere Klassen bereit stellt verwendet.
-
philipp2100 schrieb:
nwp3 schrieb:
Die display-Funktion hat doch alles. Sie hat über den Parameter Zugriff auf den öffentlichen Zustand der Programmlogik und über die Toolkit::-Zugehörigkeit Zugriff auf sämtliche Fensterfunktionen. Was fehlt denn?
Eine generische display()-Funktion, die Teil eines Toolkits ist, das VOR dem eigentlichen Programm entworfen wird, kann doch gar nichts von der Programmlogik wissen, die sie darstellen soll.
Klar kann sie. Wir programmieren jetzt mal StarCraft nach:
struct Unit{ enum class Type{Zergling, Zealot, Siegetank, ...}; Type t; //kram hier unten interessiert für display eigentlich nicht int currentHP; int maxHP; //Funktion wegen upgrades int damage; int defence; }; struct Logic{ vector<Unit> units; //Rumrennende Einheiten vector<Object> objects; //Fliegende Schüsse und sonstiges private: //Unwichtiger Kram }; void display(const Logic &l){ for (auto &u : l.units){ renderUnit(u); } renderMap(); //map nach den Einheiten rendern weil pixel nicht überschrieben werden }Da fehlen noch ein paar Details. Der Punkt ist, dass man das schreiben kann bevor man diese kennt.
Vielleicht ist das auch eine Glaubensfrage ob man Programme Top-Down oder Bottom-Up schreibt. Ich bin für Top-Down, du scheinbar nicht.philipp2100 schrieb:
nwp3 schrieb:
"verwaltet Speicher für" gibt es in so einer Anwendung gar nicht.
Doch, sogar in jeder größeren gibt es eine Speicherverwaltung, die selbstverständlich auch objektorientiert designt werden kann.
Ich glaube wir haben unterschiedliche Vorstellungen was eine Anwendung ist. Mein Verständnis von Anwendung ist sowas wie StarCraft. StarCraft hat die Anforderung, dass Zerglinge rummrennen und Gebäude fressen können. StarCraft hat niemals die Anforderung, dass ein Zergling ein Objekt einer Klasse ist. StarCraft hat auch nicht die Anforderung, dass die Einheiten in einem vector stecken oder in einer abstrakten Liste und stellt allgemein keine Anforderungen an Speicher. Nur Compiler, Betriebssystem und VMs tun sowas. Wenn man selbst eine Speicherverwaltungsklasse schreiben will, dann gibt es dafür natürlich Anforderungen. Die sind aber von der Natur "haben weniger als 10% Platz-Overhead und wenig Cache-Misses und implementieren diese Funktionen in O(x)" und nicht "leitet privat von Klasse X ab". Damit sind die dann auch von sich aus austauschbar ohne verbungs-cast-Gefrickel.
-
nwp3 schrieb:
philipp2100 schrieb:
Eine generische display()-Funktion, die Teil eines Toolkits ist, das VOR dem eigentlichen Programm entworfen wird, kann doch gar nichts von der Programmlogik wissen, die sie darstellen soll.
Klar kann sie. Wir programmieren jetzt mal StarCraft nach:
struct Unit{ enum class Type{Zergling, Zealot, Siegetank, ...}; Type t; //kram hier unten interessiert für display eigentlich nicht int currentHP; int maxHP; //Funktion wegen upgrades int damage; int defence; }; struct Logic{ vector<Unit> units; //Rumrennende Einheiten vector<Object> objects; //Fliegende Schüsse und sonstiges private: //Unwichtiger Kram }; void display(const Logic &l){ for (auto &u : l.units){ renderUnit(u); } renderMap(); //map nach den Einheiten rendern weil pixel nicht überschrieben werden }Damit hast du jetzt ein Interface geschaffen, das jedes Programm auf Biegen und Brechen benutzen muss und nicht eben das schönste, würde ich sagen. Stell dir mal vor, GTK hätte so ein Interface. Klar das würde fürs Anzeigen zunächst gehen, aber was ist mit Interaktionen wieder zurück, z.B. reagieren auf Mausklicks?
nwp3 schrieb:
Ich glaube wir haben unterschiedliche Vorstellungen was eine Anwendung ist.
Eher davon was Design ist.
nwp3 schrieb:
StarCraft hat auch nicht die Anforderung, dass die Einheiten in einem vector stecken oder in einer abstrakten Liste und stellt allgemein keine Anforderungen an Speicher. Nur Compiler, Betriebssystem und VMs tun sowas. Wenn man selbst eine Speicherverwaltungsklasse schreiben will, dann gibt es dafür natürlich Anforderungen.
Wie gesagt, es geht hier aber um Designüberlegungen und nicht um Anforderungen.
Und Design beinhaltet natürlich auch ganz klar das Zusammenspiel der Klassen bzw. welche Klassen es überhaupt gibt usw.edit/PS: Lass uns diese Diskussion vllt. lieber in einen anderen Thread oder per PN fortsetzen, wir sind ja schon ziemlich offtopic hier..
-
Meiner schrieb:
Da ich noch nie wirklich Programme mit Guis geschrieben habe, weiß ich aber nicht so recht wie ich nun weiter vorzugehen habe, bzw. wie ein Klassendesign diesbezüglich aussehen könnte.
Nehmen wir mal an, ich habe nun ein Programm mit der fertigen Programmlogik. Wie genau würde man nun das User-Interface kapseln (oder abstrahieren, etc.) und wie gestaltet man es so, dass ein Austausch der verwendeten Toolkits etc. noch möglich wird?Um vielleicht auf das Thema zurückzukommen... Ich denke, es ist kaum möglich, bei der GUI Programmierung besonders viel zu abstrahieren. GUI Programmierung ist oft nämlich komplexer, als viele annehmen. Es geht ganz selten darum, irgendein Fenster zusammenzuklicken, wo man 2-3 Werte einstellen kann, zumindest bei uns. Wir entwicklen mit Qt und unsere GUI ist ziemlich komplex und wird auch ständig komplexer und da muss man alle Möglichkeiten des Frameworks ausschöpfen (und das erweitern). Und da unterscheiden sich die Frameworks zu stark, um hier sinnvoll viel abstrahieren zu können. Wenn wir z.B. mal sowas wie einen Einstellungsdialog für eine Suchfunktion einbauen, kommen ziemlich sicher sofort zig Ideen und Anforderungen von allen möglichen Kunden und Consultants. Weil die Software einfach so komplex und umfangreich ist, dass es sehr viele Möglichkeiten und viele Informationen gibt und da will keiner irgendwelche Fenster ala Windows 3.1 sehen. Und dann schreibt man halt in Qt zig Models, ProxyModels, ItemDelegates, eigene Controls, Validatoren Ableitungen von Styles usw. Und das ist schon ziemlich frameworkspezifisch, in Gtk würde man das wohl komplett anders machen. Da will ich sicher keine zusätzliche Abstraktionsschicht dazwischenbauen, da dreht man ja durch. Dann gibts noch andere Aspekte, die das alles komplizierter machen, weil die GUI muss z.B. verzögerungsfrei reagieren, deswegen werden viele Sachen in Seitenthreads ausgeführt. Ist aber oft auch nicht einfach, weil für einige Sachen verwendet man am besten einen Threadpool, und andere muss man zwar im Hintergrund ausführen, aber serialisieren, weil das sonst kontraproduktiv wäre. Und einige der Hintergrundjobs muss man auch zusammenfassen, um nicht zu viel Traffic oder zu viele Anfragen zu erzeugen. Das ist dann wieder nicht so wirklich vom Framework abhängig und kann wiederverwendet werden, aber das sind Sachen, an die man vorher nicht unbedingt denkt und die alles komplizierter machen. Und dann kommen noch ganz lustige Sachen, wenn man auf irgendwelche sehr speziellen Probleme eingehen muss, weil ein Partner z.B. paar von unseren Fenstern als Plugin in einem exotisch CAD System verwendet und dabei sporadisch Probleme auftreten, wo man dann schlimmstenfalls Wochen debuggen muss, um den Fehler oder einen Workaround zu finden.
-
nwp3 schrieb:
Da fehlen noch ein paar Details. Der Punkt ist, dass man das schreiben kann bevor man diese kennt.
Die von dir gezeigte Display-Funktion ist mit ihren 3 1/2 Zeilen völlig trivial, und damit völlig uninteressant. Und je nicht-trivialer und interessanter sie wird, desto mehr schränkt sie ein.
Oft so sehr, dass sie einem eine weitere Vorgehensweise aufzwingt, die einfach überhaupt nicht gut ist. Weswegen es z.B. auch GUI Toolkits gibt die für bestimmte Dinge einfach überhaupt nicht gut geeignet sind.nwp3 schrieb:
Da fehlen noch ein paar Details. Der Punkt ist, dass man das schreiben kann bevor man diese kennt.
Vielleicht ist das auch eine Glaubensfrage ob man Programme Top-Down oder Bottom-Up schreibt. Ich bin für Top-Down, du scheinbar nicht.Nach meiner Erfahrung funktionieren weder Top-Down noch Bottom-Up ordentlich.
Ich programmiere meine Sachen eher nach Auftragen-Polieren.
Wobei man natürlich trotzdem irgendwo anfangen muss, und da gehe ich die Sachen meist eher von unten an. Weil man damit schneller ausführbaren und vor allem testbaren Code hat.
-
BTT:
Viele GUI Toolkits sind bereits Bridges. Diese dann nochmal zu Bridgen macht eher wenig Sinn.
Also wieso einen eigenen Wrapper für Qt, Gtk und weiss-noch-was schreiben, wenn es Qt und Gtk doch eh schon für Windows, *NIX und OS-X gibt?Macht mMn. nur Sinn, wenn es für das Set an zu unterstützenden Plattformen nix gibt. Und dass das dann einen grossen Aufwand darstellt, sollte auch klar sein. Man muss sich bloss mal angucken wie gross die GUI Teile von Qt, Gtk etc. sind.
Dann vielleicht eher gucken dass man die Software möglichst sauber trennt. Also möglichst die Controller-Logik nicht in die Window-Klassen reinhacken wie es sonst üblich ist, sondern diese wirklich in eigene Klassen raustrennen. Dann kann man zumindest Model und Controller gemeinsam lassen, und muss "nur" noch für jede Plattform (bzw. jedes GUI Toolkit) eigene Views schreiben.