polymorphe Objekthierachie kapseln



  • Hallo,
    ich versuche mich gerade an einer kleinen GUI-Lib, die (zumindest) auf Linux und Windows das native Zeichnen von Festern u.ä. ermöglichen soll. Ich bediene mich dabei dem "Abstract Factory Pattern", indem ich für die verschiedene Betriebssysteme jeweils verschiedene "GUI-Factories" implementiere, was dann ungefähr so aussieht:

    // GuiFactory.h
    class GuiFactory{
    public:
       virtual GuiWindow* CreateWindow() =0;
    }
    
    // Win32Factory.h
    #include "GuiFactory.h"
    #include "Win32Window.h"    
    class Win32Factory : public GuiFactory{
    public:
       GuiWindow* CreateWindow(){
           return new Win32Window();
           // Win32Window erbt von GuiWindow
       }
    }
    

    Im Programm existiert dann jeweils eine globale Instanz der GuiFactory und die Instanzierung von neuen Fenstern schaut so aus:

    #ifdef WIN32
            GuiFactory* GUI_FACTORY = new Win32Factory();
        #endif
        //...
    
        GuiWindow *myWindow = GUI_FACTORY->CreateWindow();
    

    Allerdings finde ich diesen Ansatz eher "hässlich" und schwer zu warten, weil ich überall im System mit Zeigern rumhantieren muss.
    Gibt es hier einen alternativen Ansatz, der vielleicht auf eine Lösung per Templates hinausläuft? Oder eine Möglichkeit, mit der ich diese Objekthierachie kapseln kann?

    Mein zweites Problem betrifft die Umsetzung des betriebssystemspezifischen "Event-Listeners". In der Windowsimplementierung fängt eine Klasse Nachrichten mithilfe einer statischen WindowProc-Funktion und leitet sie an eine Instanz meines Event-Listeners. Dabei werden automatisch die betriebssystemsspezifischen Nachrichten in meine Lib-spezifischen Nachrichten mittels einer Abbildung(in dem Falle eine große std::map) umgesetzt.
    Wie lösen die anderen Gui-Libs diese Vorhaben? Gibts es alternative Möglichkeiten, die mich weniger Abhängig von den betriebssystemspezifischen Methoden der Nachrichtenbehandlung machen?



  • Hallo Matzer,

    zumal du nie zwei verschiedene Versionen einer dieser Window-Klassen (gleichzeitig) kompilieren wirst,
    ist Polymorpie sicherlich der falsche Weg.

    Ich würde sowas etwa so machen:

    // plattformunabhängig includes
    
    #include <string>
    
    // plattformabhängige includes
    
    #ifdef WIN32
    #include <windows.h>
    #endif
    
    class Window{
    
    // Bereich für Funktionen, Konstruktoren und Member-Variablen, die in deiner Lib plattform-unabhängig implementiert sind
    
    public:
    	Window(const std::string& title);
    	~Window();
    	void show();
    	void setTitle(const std::string& title); // setzen der Variablen und setTitleInternal aufruf
    
    private:
    	std::string title;
    
    // Bereich für Funktionen, Konstruktoren und Member-Variablen, die in deiner Lib plattform-abhängig sind, aber auf jeder Plattform vorhanden
    
    	void setTitleInternal(const std::string& title); // Zugriff über das Handle um Fenster umzubennenen
    
    // Bereich für Funktionen, Konstruktoren und Member-Variablen, die plattformabhängige Daten enthalten
    
    #ifdef WIN32
    private:
    	HWND hwnd;
    #endif
    
    };
    

    In den .cpp-Dateien kannst du dann die unteren beiden Bereiche sauber in _win.cpp und _linux.cpp untergliedern.

    Erste Möglichkeit:
    window.cpp:

    // plattformunabhängige Konstruktoren und Funktionen
    // ...
    
    // include der separaten Datein
    #ifdef WIN32
    #include "window_win.cpp"
    #endif
    

    Zweite Möglichkeit:
    Je nach betriebssystem unterschiedliche make-files, die dir dann sowohl
    window.cpp als auch z.B. window_win.cpp separat kompilieren.

    Gruß,
    CSpille



  • Ok, vielen Dank erstmal für den Hinweis. Ich habe nur leider unterschlagen, dass die Klasse GuiWindow wieder von einer weiteren Klasse "GuiBase" erbt. D.h. von dieser einen Basisklasse erben zunächst einmal alle abstrakten Klassen:

    class GuiBase{
       public:
       virtual void Show()=0;
       // alle Methoden, die von allen Gui-Objekten unterstützt werden sollen
    };
    
    class GuiWindow : public GuiBase{
       public:
       virtual void Show()=0;
    };
    
    class Win32Window : public GuiWindow{
       public:
       void Show(){
           //Implementierung
       }
    };
    

    Kann ich in diesem Fall überhaupt auf Polymorphie verzichten?



  • Hi Matzer,

    du brauchst nicht krampfhaft versuchen auf Polymorphie zu verzichten 😉

    Ich wollte nur sagen, dass es keinen Sinn macht eine (vituelle) Basisklasse zu erstellen,
    die nur von einer Klasse (pro Betriebssystem) erbt.

    Die Win32Window-Klasse würde ich mir sparen und in der GuiWindow-Klasse das
    machen, was ich vorher beschrieben habe.

    Gruß,
    CSpille



  • du brauchst nicht krampfhaft versuchen auf Polymorphie zu verzichten 😉

    Ich versuche nur auf möglichst viel Zeiger-"Gefrickel" zu verzichten 😃 Und meistens finde ich Template-Ansätze meist deutlich schöner, weswegen ich nach einem alternativen Weg der Implementierung gefragt hatte.
    Wobei man an dieser Stelle natürlich sagen muss, dass eine GUI-Library für Polymorphie praktisch prädestiniert ist.
    Letztlich hat man aber überall im Code mit Zeigern zu hantieren, wobei sich spätestens bei der Freigabe des Speichers die Frage stellt, welche Klasse diese Aufgabe nun übernehmen soll. Vielleicht sollte ich es mal erstnhaft in Betracht ziehen, shared_ptr zu verwenden 🙂

    Bezüglich meines Problems mit der Nachrichtenbehandlung bin ich nun auf boost::asio gestoßen. Ich war bisher felsenfest davon überzeugt, dass die Lib nur für die "Netzwerkprogrammierung" interessant zu seien scheint, aber tatsächlich lässt sich damit ziemlich elegant ein Event-Listener implementieren. Mal gucken was ich draus machen kann ^^.

    Und nochmals danke für den Hinweis!



  • Ich glaube, ich würde für die betriebssystemspezifischen Klassen tatsächlich auf Polymorphie verzichten. Vielmehr würde ich mehrere parallele Hierarchien implementieren, für jedes OS eine. Die Klassen dort müssten natürlich identische (public) Schnittstellen haben. Für den User des Frameworks (und für nicht spezifische Klassen des Frameworks) würde ich lediglich typedefs definieren, die eine spezifische Klasse auf einen allgemeinen Namen abbilden.

    --- Datei Win32EditField.hpp ---
    class Win32EditField: public GuiBase {
    
       // ...
    
    };
    
    --- Datei LinuxEditField.hpp ---
    class LinuxEditField: public GuiBase {
    
       // ...
    
    };
    
    --- Datei EditField.hpp ---
    #ifdef COMPILE_WINDOWS
       #include "Win32EditField.hpp"
       #define EditField Win32EditField
    #else
       #include "LinuxEditField.hpp"
       #define EditField LinuxEditField
    #endif
    

    Natürlich müsste man dann in den Makefiles dafür sorgen, dass nur die passenden Dateien gebaut werden.



  • CSpille

    zumal du nie zwei verschiedene Versionen einer dieser Window-Klassen (gleichzeitig) kompilieren wirst,

    Ach?

    ist Polymorpie sicherlich der falsche Weg.

    Mal die fragwürdigkeit der ersten Aussage ausser Acht gelassen...:
    Wieso?

    Ich versuche nur auf möglichst viel Zeiger-"Gefrickel" zu verzichten

    Dann frickel halt nicht. Nimm halt Smart Pointer.



  • hustbaer schrieb:

    CSpille

    zumal du nie zwei verschiedene Versionen einer dieser Window-Klassen (gleichzeitig) kompilieren wirst,

    Ach?

    ist Polymorpie sicherlich der falsche Weg.

    Mal die fragwürdigkeit der ersten Aussage ausser Acht gelassen...:
    Wieso?

    Ich versuche nur auf möglichst viel Zeiger-"Gefrickel" zu verzichten

    Dann frickel halt nicht. Nimm halt Smart Pointer.

    1. Er wird niemals zwei verschiedene Versionen der Window-Klassen kompilieren, weil Win32Window wohl kaum unter Linux und LinuxWindow wohl kaum unter Windows compiliert. Wieso also "Ach?"?

    2. In sofern ist Polymorphie definitiv der falsche Weg.

    3. Unter "Zeiger-Gefrickel" verstand ich, dass wirklich ausschließlich Zeiger verwendet werden können (ob smart oder nicht), wenn man so vorgeht. Das ist Gefrickel und vollkommen unangemessen. Ich würde mich mit einer solchen Libaray nicht wohlfühlen.

    Man sehe sich Qt an. Da können Controls wie ganz normale Objekte beispielsweise auch als Instanzvariablen angelegt werden. Keine AbstractFactorys.



  • Matzer schrieb:

    Ich versuche nur auf möglichst viel Zeiger-"Gefrickel" zu verzichten

    hustbaer schrieb:

    Dann frickel halt nicht. Nimm halt Smart Pointer.

    Grautvornix schrieb:

    3. Unter "Zeiger-Gefrickel" verstand ich, dass wirklich ausschließlich Zeiger verwendet werden können (ob smart oder nicht), wenn man so vorgeht. Das ist Gefrickel und vollkommen unangemessen. Ich würde mich mit einer solchen Libaray nicht wohlfühlen.

    Man sehe sich Qt an. Da können Controls wie ganz normale Objekte beispielsweise auch als Instanzvariablen angelegt werden. Keine AbstractFactorys.

    Genau das meinte ich. Nach außen hin, d.h. für den Benutzer der Lib (in dem Fall wohl nur ich 🙂 ) sollten so wenig Zeiger wie möglich verwendet werden. Dies ist aber durch das Design der Abstract Factory praktisch unmöglich. Und das macht die Lib unangenehm zu benutzen. Insbesondere solche Konstrukte:

    GuiFactory->CreateWindow();
    

    finde ich persönlich ziemlich hässlich.
    In diesem Fall sollte ich die Version von Grautvornix vorziehen und die betriebssystemspezifischen Sachen jeweils in separate Hierachien einteilen.



  • Matzer schrieb:

    Genau das meinte ich. Nach außen hin, d.h. für den Benutzer der Lib (in dem Fall wohl nur ich 🙂 ) sollten so wenig Zeiger wie möglich verwendet werden. Dies ist aber durch das Design der Abstract Factory praktisch unmöglich. Und das macht die Lib unangenehm zu benutzen. Insbesondere solche Konstrukte:

    GuiFactory->CreateWindow();
    

    finde ich persönlich ziemlich hässlich.
    In diesem Fall sollte ich die Version von Grautvornix vorziehen und die betriebssystemspezifischen Sachen jeweils in separate Hierachien einteilen.

    Der User (in diesem Fall wohl nur du 😉 ) wird's dir danken!

    Ist dir übrigens klar, was du dir mit einer solchen Library antust?! Ich habe dasselbe auch schonmal gemacht, und zwar nur für Windows, also nicht einmal portabel. Das hat sich gelohnt, war aber ne Wahnsinnsarbeit. Damit habe ich mir dann meinen Nick redlich verdient 😉

    Stefan aka Grautvornix.


Anmelden zum Antworten