Singleton Problem



  • Morle schrieb:

    hustbaer schrieb:

    @dot
    Der Anwendungsfall ist quasi immer "ich müsste ja sonst überall den Zeiger durchzeichen".

    Was übrigens bei modularer Entwicklung ein sehr legitimier Anwendungsfall ist.

    Wobei Singletons ja gerade dem Prinzip der Modularität widersprechen, da man die Funktionalität nicht mehr lokal hält.

    Zeiger immer durchreichen kann natürlich mühsam sein. Teilweise gibt es aber recht elegante Lösungen, wenn man die Zeiger verpackt oder Zwischen-APIs einbaut. Wenn man sich etwas achtet, kann man meiner Meinung nach die meisten Singletons vermeiden, ohne dass der Code besonders umständlich wird. Die Verlockung ist halt gross, den einfachen Weg zu wählen.



  • Nexus schrieb:

    Zeiger immer durchreichen kann natürlich mühsam sein. Teilweise gibt es aber recht elegante Lösungen, wenn man die Zeiger verpackt oder Zwischen-APIs einbaut.

    Ich wäre an der eleganten Lösung sehr interessiert. Ohne Durchreichen geht es IMHO nicht, denn wenn man mehrere Zeiger durchzureichen vermeiden will, reicht man wenigstens immer einen Zeiger auf irgendein "globales" Objekt durch, von dem man dann wieder die anderen Zeiger erhalten kann.



  • @Morle
    Ja, nur dass das "globale" Objekt auf das man da einen Zeiger rumreicht eben nicht global ist. Genau das ist ja der Knackpunkt.

    ----

    Klar, Singletons sind nicht von Grund auf böse und zu nichts zu gebrauchen. Singletons sind aber fast immer eine Notlösung, ein unsauberer Hack der manchmal als Kompromiss Sinn macht. Singletons sind nichts was man sich angewöhnen sollte.

    Die Fälle wo ich bisher gezwungen war ein Singleton zu verwenden hatten eigentlich immer als Ursache irgend eine Library die zu doof für irgendwas war.
    Also z.B. Libraries die Init() / Release() Calls haben die nicht mitzählen (= Release() gibt sofort frei, auch wenn man 2x Init() gemacht hat), Libraries die nicht threadsafe sind, Libraries die Callbackfunktionen ohne void* userdata Parameter verwenden uswusf.

    Und in den (wenigen) Fällen wo ich bisher Singletons als sinnvolle Lösung angesehen (und dann auch verwendet) habe, hatten diese Singletons keinen mutable State. Beispielsweise eine Sammlung von Lookup-Tables die man per Code initialisieren möchte, aber nur wenn sie auch gebraucht werden.
    (Wobei die Frage ist ob man das als Singleton implementieren muss, ein call_once auf eine Initialisierungsfunktion hätte es wohl auch getan.)



  • Morle schrieb:

    Ich wäre an der eleganten Lösung sehr interessiert.

    Das ist natürlich von der Problemstellung abhängig.
    Gib ein möglichst konkretes Beispiel, dann können wir gucken welche eleganten Möglichkeiten uns da einfallen.



  • hustbaer schrieb:

    Morle schrieb:

    Ich wäre an der eleganten Lösung sehr interessiert.

    Das ist natürlich von der Problemstellung abhängig.
    Gib ein möglichst konkretes Beispiel, dann können wir gucken welche eleganten Möglichkeiten uns da einfallen.

    z.B. das Beispiel, was ich vorher in diesem Thread gebracht habe:

    Ein Hauptprogramm wird gegen 3 an sich unabhängige Module gelinkt, die jeweils als LIB vorliegen. Diese Module manipulieren alle auf irgendeine Weise Daten, die in Dateiform vorliegen. Dazu gibt eine (ältere, nicht threadsafe) DLL, die ein Datenmodell aus solchen Datendateien bauen kann, welches man dann wiederrum manipulieren und abspeichern kann.
    Das Hauptprogramm sollte IMHO nichts davon wissen müssen, wie die einzelnen Module ihre Datenmanipulation vornehmen oder welche sonstigen Abhängigkeiten die Module intern haben. Weswegen die Singletonlösung im Moment verwendet wird.

    Dies ermöglichte unter anderem auch das stillschweigende Upgrade eines 4ten Moduls auf eine neuere DLL zur Datenmanipulation (die diese Probleme der älteren nicht hat), ohne das irgendwelche anderen Programmbestandteile geändert werden mussten.



  • Mit anderen Worten, statt Objekten werden DLLs verwendet. So eine Architektur habe ich zum Glück seit längerer Zeit nicht mehr sehen müssen.

    Der vernünftige Weg, das umzusetzen, ist natürlich, eine gemeinsame Basisklasse zu definieren, die eine einheitliche Schnittstelle für was auch immer ein Modul halt können soll zur Verfügung stellt, und sich von der DLL per Factory-Funktion(en) ein Objekt/Objekte dieser Klasse geben zu lassen. Die DLL kennt intern eine Kindklasse dieser Basisklasse (die das Hauptprogramm nicht kennen muss) und erzeugt aus dieser ganz normal konkrete Objekte.

    Die Funktionen, die deine DLLs derzeit exportieren, werden zu virtuellen Methoden der Basisklasse, und du kommst nicht in die Verlegenheit, in einer DLL veränderbaren, globalen Zustand halten zu müssen.

    Im Übrigen ist, selbst wenn man eine derartige Architektur voraussetzt (und das sollte man wirklich nicht tun, wenn es sich irgendwie vermeiden lässt), nicht einzusehen, warum dein globales Objekt Singleton-Code drumherum braucht. Das Kind ist zwar mit "global" bereits in den Brunnen gefallen, also ist das nicht mehr das Hauptproblem, aber wenn du glaubst, es damit wieder rausfischen zu können, ist es nicht nur nass, sondern auch schief gewickelt.



  • seldon schrieb:

    Der vernünftige Weg, das umzusetzen, ist natürlich, eine gemeinsame Basisklasse zu definieren, die eine einheitliche Schnittstelle für was auch immer ein Modul halt können soll zur Verfügung stellt, und sich von der DLL per Factory-Funktion(en) ein Objekt/Objekte dieser Klasse geben zu lassen. Die DLL kennt intern eine Kindklasse dieser Basisklasse (die das Hauptprogramm nicht kennen muss) und erzeugt aus dieser ganz normal konkrete Objekte.

    So wird es in der neuen Variante gemacht. Ist an COM angelehnt.

    Im Übrigen ist, selbst wenn man eine derartige Architektur voraussetzt (und das sollte man wirklich nicht tun, wenn es sich irgendwie vermeiden lässt), nicht einzusehen, warum dein globales Objekt Singleton-Code drumherum braucht.

    Es "braucht" ihn nicht. Es ist aber für mich einfacher, denn ich kann im Clientcode schreiben:

    MyDLL::GetInstance().Funktion(pDaten);
    

    Funktion sieht dann in etwa so aus:

    void MyDLL::Funktion(void *p)
    {
     Lock lock(mLock);
     _dllFunktion(p);
    }
    

    Wie gesagt, es "geht" natürlich auch mit einem globalen Pointer, den ich dann erstmal auf Gültigkeit prüfen (was mir das Singleton abnimmt) und dann nochmal explizit das Lock anfordern muss (was ich auch mit freien Funktionen lösen könnte).
    Die Singletonlösung ziehe ich dennoch vor, denn sie hält alles relevante einigermaßen elegant "zusammen". Aber vielleicht kommt ja hier noch ne andere Lösung 🙂



  • Ich sehe nicht, welchen Vorteil das gegenüber einem Namensraum hat.

    // Header
    namespace MyDLL {
      void Funktion(void *p);
    }
    
    ...
    
    // Source
    namespace MyDLL {
      namespace {
        struct {
          Mutex mLock;
          // Mehr Kram, wenn notwendig.
        } global_data;
      }
    
      void Funktion(void *p) {
        Lock lock(global_data.mLock);
        _dllFunktion(p);
      }
    }
    
    ...
    
    // Anwendung
    MyDLL::Funktion(foo);
    

    Natürlich handelt es sich dabei im Grunde auch nur um Kosmetik. Es hindert ja nichts irgendwelchen Code daran, _dllFunktion (was übrigens ein reservierter Bezeichner ist, den du gar nicht verwenden darfst), direkt aufzurufen.

    Das Grundproblem bleibt der globale Zustand, und dieses ganze Konstrukt ist ein ziemliches Kartenhaus. Gelöst werden müsste das in der DLL, um die es geht.



  • Ich hoffe wir reden jetzt nicht aneinander vorbei. Der Zugriffsfunktionen auf die DLL-Funktionen in die Singletonklasse zu packen lag lediglich nahe. Der eigentliche Zweck des Singletons ist aber sicherzustellen (nichts anderes macht MyDLL::GetInstance()), dass _dllFunktion ein gültiger Funktionspointer ist. Deine Lösung erfordert IMHO wenigstens noch ein if um die _dllFunktion herum. Wir bewegen uns hier aber wohl schon im Bereich des persönlichen Geschmacks.



  • Ich finde die Diskussion hier nicht zielführend. Warum? Weil hier überhaupt keine Basis dafür besteht, welche Nachteile Singletons haben ("Das weiß doch jeder" ist nicht adäquat, denn das stimmt 1. nicht und 2. braucht man dennoch eine gemeinsame Basis).

    Ob Singleton in bestimmten Situationen dann ein Anti-Pattern ist oder nicht, spiegelt sich dann darin wieder, ob die Nachteile dort noch gelten. Das wäre Mal ein systematischer Ansatz.

    Nehmen wir Mal an, wikipedia wäre eine gute Quelle und nehmen die Nachteile:
    http://de.wikipedia.org/wiki/Singleton_(Entwurfsmuster)#Nachteile

    - Äquivalent zu globalen Variablen erstellen
    => globale Variablen sind der der Alternativvorschlag hier, fällt also weg

    - Verschleierte Abhängigkeit + Kopplung
    => Massive Argumente, trifft beides auch eingeschränkt auf globale Variablen zu, wobei die wenigstens im Kopf der Quellcodedateien auftauchen

    - Einzigartigkeit in MT-Umgebungen nicht gegeben (wurde ja oben bereits diskutiert)

    - Das Testen eines Singleton kann kompliziert sein.
    => Das ist ein Nachteil

    - Achtung bei Initialisierungs-Reihenfolge
    => ebenso bei globalen Variablen

    - Eine Ressource-Deallokation von Ressourcen, die das Singleton verwendet, ist schwierig
    => Ist hier vermutlich egal, wäre bei einer globalen Variable aber wohl ähnlich

    Oder kurz:
    Was ist an einer globalen Variable so viel besser? Singletons als Anti-Patterns zu verneinen, aber globale Variablen einzuführen erfordert mMn eine gute Begründung.



  • @Morle: In dem Fall etwas wie (aufs Wesentliche zusammengestrichen)

    // Header
    namespace MyDLL {
      void Funktion(void *p);
    }
    
    ...
    
    // Source
    namespace MyDLL {
      namespace {
        struct dirty_dll_handler {
          dirty_dll_handler(char const *dll_name, int flags) {
            dll_handle = dlopen(dll_name, flags);
            dllFunktion = static_cast<void(*)(void*)>(dlsym(dll_handle, "dllFunktion"));
          }
    
          Mutex mLock;
    
          void *dll_handle;
          void (*dllFunktion)(void*);
        };
    
        dirty_dll_handler global_data("old_dirty.dll", RTLD_LAZY);
      }
    
      void Funktion(void *p) {
        Lock lock(global_data.mLock);
        global_data._dllFunktion(p);
      }
    }
    

    Ein Singleton ist da weder notwendig noch vorteilhaft. Die Klasse ist auch logisch kein Singleton, weil sie sich, wenn du mehrere solcher alten DLLs abhandeln musst, wiederverwenden ließe.

    @Eisflamme: An einer globalen Variable ist nicht wirklich was besser als an einem Singleton. Es ist umgekehrt auch nicht wirklich was besser an einem Singleton als an einer globalen Variable. Wie gesagt, das Problem müsste eigentlich in der DLL gelöst werden, die selbst globale Variablen zu halten scheint.



  • Wie macht ihr Logging, wenn nicht als Singleton und es möglich sein soll, dass mehrere Thread ins gleiche Logfile schreiben, ohne das die Messages zerstückelt werden und das Logfile soll bis zum beenden immer geöffnet sein, damit keiner die Schreibrechte klaut? Eine Logging Klasse in jede andere Klasse durchzureichen ist doch total umständlich.



  • Man nehme ein globals std::ostream Objekt?



  • Nathan schrieb:

    Man nehme ein globals std::ostream Objekt?

    Also sowas wie std::cout!? Wieso sollte sowas ein Singleton sein?



  • Nathan schrieb:

    Man nehme ein globals std::ostream Objekt?

    Also wieder globale Variablen, statt Singletons. Weil Singletons jetzt böse sind und globale Variablen es früher waren, aber das schon alle vergessen haben.



  • Singleton und globale Variablen sind zwei völlig voneinander unabhängige Dinge...



  • dot schrieb:

    Singleton und globale Variablen sind zwei völlig voneinander unabhängige Dinge...

    Nein, in C++ zumindest kann man keine Singletons ohne globale/statische Variablen implementieren. Hatten wir übrigens schon 🙂

    Oder anders ausgedrückt: Das Singleton-Pattern ist nicht die logische Konsequenz daraus, eine Klasse nur einmal instanziieren zu wollen. Zusätzlich muss auch das Bedürfnis nach einem gemeinsamen Zugriffspunkt bestehen.



  • dot schrieb:

    Nathan schrieb:

    Man nehme ein globals std::ostream Objekt?

    Also sowas wie std::cout!? Wieso sollte sowas ein Singleton sein?

    Ist eine globale Variable ein Singleton?
    Bleiben wir beim Logger, std::cout, std::clog, std::cerr + eigener Logger sind alles Instanzen von std::ostream. Nach einer Instanz sieht das nicht gerade aus.

    looogggeer schrieb:

    Nathan schrieb:

    Man nehme ein globals std::ostream Objekt?

    Also wieder globale Variablen, statt Singletons. Weil Singletons jetzt böse sind und globale Variablen es früher waren, aber das schon alle vergessen haben.

    Wahl zwischen Pest und Cholera. :p

    Nein, std::cout ist eine begründete globale Variable. Da gibt es auch keine Alternative, weil std::ostream kein Singleton ist.



  • Ach, hier stand Käse.



  • @Morle
    Jo.
    Dein Beispiel fällt für mich in die selbe Kategorie wie auch non-nesting Init/Release Funktionen.

    Bei sowas finde ich nen "Singleton" als Workaround OK. Wobei ich hier auch normale freie Funktionen als Wrapper vorziehen würde.
    Irgendwo muss es aber irgend ein "Singleton" geben, weil es darf ja nur eine Mutex geben. (Wobei dieses "Singleton" nicht unbedingt das "Singleton-Pattern" verwenden muss, es tut's auch eine globale Variable + call_once o.ä.)

    Welche Dinge als Workaround OK sind oder nicht finde ich aber nicht wirklich ein sehr spannendes Thema. Was sein muss muss eben sein.

    Hast du auch ein Beispiel wo ein Singleton vorkommt das nicht Teil eines Workarounds ist?

    pS: Bei deinem Beispiel einen Zeiger auf irgend eine Wrapper-Klasse rumzureichen wäre mMn. komplett verkehrt. Würde nur einen weiteren unnötigen Bruchpunkt schaffen.


Anmelden zum Antworten