Designfrage: Singleton "installieren"



  • Tach auch,

    ich habe gerade ein kleines Designproblem in meiner library:
    Es gibt ein Singleton, das den zentralen Logger zur Verfügung stellt. Dabei soll der Nutzer der library entscheiden können, wie er ihn implementiert, ob in eine Datei oder die Konsole oder wohin auch immer geloggt werden soll.

    Derzeit ist das einfach so gemacht, dass in der library ein .h-File dabei ist, das etwa so aussieht:

    /**
      * You must provide an implementation of this interface
      */
    class LogWriter
        {
        public:
            virtual ~LogWriter() {}
    
            virtual void writeEntry(const LogEntry& entry) = 0;
        };
    
        class Singleton : private boost::noncopyable {
        public:
            static LogWriter& getLogger();
    
        private:
            Singleton();
        };
    

    In einer Applikation sieht eine mögliche Implementierung so aus:

    class ConsoleLogger: public LogWriter
    {
    public:
    
        void writeEntry(const LogEntry& entry)
        { // timestamp, fehlerklasse und text nach std::cout schreiben
        }
    };
    
    LogWriter& Singleton::getLogger()
    {
        static ConsoleLogger logger;
        return logger;
    }
    

    Das gefällt mir aus zwei Gründen nicht:
    -Erstens bürdet es dem Benutzer auf, auch noch den Boilerplate für die Singleton::getLogger() Methode zu schreiben, die ja bis auf den Klassennamen des konkreten Loggers immer gleich wäre.
    -Zweitens ist es so nicht möglich, die library auch als dynamische library auszuliefern, da die dynamische Lib ja dann eine undefined reference auf die Singleton::getLogger Funktion hätte.

    Was ich jetzt gerne hätte, wäre die Möglichkeit statisch einen Logger im Singleton installieren zu können. Statisch deswegen, weil ja möglicherweise schon beim ersten Erstellen irgendeiner Klasse ein log-würdiger Fehler auftreten könnte.

    Also: Wie kriege ich es hin, dass das Singleton standardmäßig mit beispielweise einem Console-Logger einkompiliert wird (so dass es auf jeden Fall in einer dynamischen Lib keine unaufgelösten Referenzen mehr gibt, selbst wenn der Library-User keinen eigenen Logger implementiert), aber wenn eine solche Implementierung vorliegt, sie im Singleton "installiert" wird.

    Gruß,
    Phil



  • Hallo

    Ich würde dir an dieser stelle empfehlen über die mögliche verwendung von "Strategy Pattern". Und entsprechend zur Auslieferung entwirfst du eines für die Console das Standardmäßig benutzt wird, und wenn der User was eigenes möchte, dann braucht er such _nur_ ums programmieren der Ausgabe kümmern.

    kurze Beispiel (Aus dem kopf und so bitte nicht direkt benutzen):

    struct IWriter {
       void writeOut( string sToOut ) = 0;
    };
    
    class ToConsole : public IWriter {
       void writeOut( string sToOut ) {
          cout << time( void ) << ": " << sToOut << endl;
       }
    };
    
    class logger {
       public:
          logger() {
             _myWriter = new ToConsole();
          }
          virtual ~logger() {
             if( _myWriter ) {
                delete _myWriter;
                _myWriter = NULL;
             }
          }
    
          void setLogger( IWriter &newWriter ) {
             if( _myWriter ) {
                delete _myWriter
             }
             _myWriter = newWriter; // Des hier müsste man aber anders machen, grad nur keine idee mehr wie.
          }
    
          void writeEntry( sEntry ) {
             _myWriter.writeOut( sEntry );
          }
          // .. und hier noch deine anderen Implementierungen und die Sachen fuers Singleton.
       private:
          IWriter *_myWriter;
    };
    

    Wie gesagt Code soll nur zur demonstration dienen. In dem Fall braucht dann ein User einfach nur von der "IWriter"-Struktur erben, seinen Klasse dann entsprechend schreiben und von dieser dann ein Objekt an die logger-Klasse übergeben.

    Mfg marco



  • Testen auf 0 ist bei delete nicht nötig.
    Das machts schon selbst.

    Simon



  • Danke für den ersten Ansatz. Habe das ganze jetzt so gemacht, allerdings mit intelligenten Zeigern.

    Einziges Problem ist, dass der Strategiewechsel immer noch zur Laufzeit passiert, und nicht schon zur Compilezeit gewechselt werden kann.

    Ich fordere ja jetzt, dass der Library-Nutzer eine Strategie implementiert und dann

    Singleton::getLogger().setStrategy(new MeineStrategy);
    

    aufruft. Wenn dieser Aufruf im Code steht, läuft das Programm ja schon. Das heißt, statisch initialisierte Objekte, die in ihren Konstruktoren was geloggt haben, haben noch die default-Strategie verwendet.

    Bei meiner alten Lösung war durch das implementieren der Singelton::getLogger Funktion ja schon zur Kompilierzeit klar, was für eine Strategie verwendet werden soll.

    Gibt's irgendeine Template-Magie nach dem Motto "aha, ich wurde implementiert, dann installier ich mich mal in der Klasse XYZ"?



  • PhilippM schrieb:

    Danke für den ersten Ansatz. Habe das ganze jetzt so gemacht, allerdings mit intelligenten Zeigern.

    Einziges Problem ist, dass der Strategiewechsel immer noch zur Laufzeit passiert, und nicht schon zur Compilezeit gewechselt werden kann.

    Ich fordere ja jetzt, dass der Library-Nutzer eine Strategie implementiert und dann

    Singleton::getLogger().setStrategy(new MeineStrategy);
    

    aufruft. Wenn dieser Aufruf im Code steht, läuft das Programm ja schon. Das heißt, statisch initialisierte Objekte, die in ihren Konstruktoren was geloggt haben, haben noch die default-Strategie verwendet.

    Bei meiner alten Lösung war durch das implementieren der Singelton::getLogger Funktion ja schon zur Kompilierzeit klar, was für eine Strategie verwendet werden soll.

    Gibt's irgendeine Template-Magie nach dem Motto "aha, ich wurde implementiert, dann installier ich mich mal in der Klasse XYZ"?

    Hy

    Ich versteh jetzt nicht ganz dein Problem, wenn du diese setStrategy-Funktion ganz am Anfang der Main aufrufst, dann wird diese doch auch für alle Konstruktoren festgelegt. Bzw. wenn du dies so in deinen Code vor den Funktionen aufrufst sollte dies funktionieren:

    namespace {
       Singleton::getLoger().setStrategy( new MeineStrategy );
    }
    

    Zumindest nehme ich diese Technik bei meinen Projekt die Möglichkeit zu haben das sie die Plugins die ich über Dlls lade sich selbst eintragen.

    Mfg marco



  • Marc-O schrieb:

    Ich versteh jetzt nicht ganz dein Problem, wenn du diese setStrategy-Funktion ganz am Anfang der Main aufrufst, dann wird diese doch auch für alle Konstruktoren festgelegt.

    Statische Objekte werden aber vor dem Eintritt in die main angelegt.

    Bzw. wenn du dies so in deinen Code vor den Funktionen aufrufst sollte dies funktionieren:

    namespace {
       Singleton::getLoger().setStrategy( new MeineStrategy );
    }
    

    Aber leider ist die Reihenfolge solcher statischen Initialisierungen über mehrere Compileunits nicht definiert. Das kann also irgendwann initialisiert werden. Dann habe ich wahrscheinlich manche Konstruktoren, die mit der default-strategy loggen, und ein paar, die mit MeineStrategy loggen. Klingt mir nicht nach einer guten Idee.

    Phil



  • Vielleicht so etwas?

    class ConsoleLogger{};
    
    template<typename T>
    class SingletonHolder : public boost::noncopyable
    {
    private:
    // ...
    
    public:
    	static T& getLogger() { static T logger; return logger; }
    
    };
    
    // Library Header Template
    typedef SingletonHolder<ConsoleLogger> Singleton;
    
    // somewhere in a file...
    class A
    {
       A() { Singleton::getLogger().writeLog("..."); }
    };
    
    A a;
    
    // ......................
    
    int main()
    {
    	Singleton::getLogger();
    }
    

    Wer keinen Logger schreibt, nutzt das Library Header Template "as is", wer einen eigenen Logger einhängt, ändert nur den Typedef

    :schland: :schland:



  • Das klingt nach einem richtig guten Plan, hat aber noch einen Schönheitsfehler:
    Wenn ich den default-Logger mit

    typedef SingletonHolder<ConsoleLogger> Singleton;
    

    installiere, der User dann aber seine eingene Implementierung baut und

    typedef SingletonHolder<MyFancyLogger> Singleton;
    

    macht, dann gibts einen conflicting typedef.



  • Herren der schwarzen Template-Magie, wo seid ihr? 🙄



  • typedef SingletonHolder<ConsoleLogger> MyConsoleLogger;
    
    typedef SingletonHolder<FancyLogger> MyFancyLogger;
    

    und alles wird gut 😮



  • Ich geb's auf und geh Schrauben sortieren ... 😞



  • PhilippM schrieb:

    Ich geb's auf und geh Schrauben sortieren ... 😞

    Das Heizöl hacken hast mir besser gefallen 😃


  • Administrator

    Wenn du eine Strategie zur Kompilezeit festlegen willst, dann mach das über Policies.

    Grüssli



  • Als dynamische Bibliothek wirst du das in der Form jedenfalls nicht hinkriegen - du willst die Strategie zur Compilezeit festlegen, und wenn die Bibliothek einmal kompiliert ist, ist die Compilezeit vorbei.

    Denkbar wäre, es zur Linkzeit zu verschieben, so dass der Benutzer dir eine andere Bibliothek hinlegen kann, aus der sich deine Klasse entsprechende Funktionen holen kann. Allerdings ist das ziemlich umständlich, insbesondere, wenn das Backend parametrisiert werden will - etwa ein Dateilogger, der einen Dateinamen braucht.



  • Dravere schrieb:

    Wenn du eine Strategie zur Kompilezeit festlegen willst, dann mach das über Policies.

    Grüssli

    Auch die sehen sher interessant aus, lösen aber nicht mein Problem:
    Auch hier muss ich ja eine standard-policy per typedef festlegen.
    Und wenn dann der Librarynutzer seine andere Policy geschrieben hat, kann er kein typedef mehr anlegen, dass diese Policy zum standard erklärt. Conflicting typedef. Ich glaub wir drehen uns gerade im Kreis!



  • Okay, neuer Vorschlag:
    Wenn wir Problem 1 nicht lösen können, dann wenigstens Problem 2:
    Gibt es eine Möglichkeit dem Linker beim Zusammenbauen der dynamsichen Library zu sagen "Das excutyble, das diese lib später benutzen wird, bietet garantiert die Funktion XYZ an", so dass ich den alten Mechanismus mit dem undefiniert lassen und erst in der Applikation definieren, in die dynamische library rüberretten kann?


  • Administrator

    PhilippM schrieb:

    Auch die sehen sher interessant aus, lösen aber nicht mein Problem:
    Auch hier muss ich ja eine standard-policy per typedef festlegen.
    Und wenn dann der Librarynutzer seine andere Policy geschrieben hat, kann er kein typedef mehr anlegen, dass diese Policy zum standard erklärt. Conflicting typedef. Ich glaub wir drehen uns gerade im Kreis!

    Moment, heisst das, dass der Logger bereits schon für die statischen und globalen Objekte deiner Bibliothek funktionieren soll, welche du als vorkompilierte DLL oder ähnliches mitliefern willst? Das kannst du kreuzweise vergessen.

    Aber was du machen kannst, ist eine Standardstrategie zu definieren, welche die Log-Einträge irgendwo abspeichert, bis der User eine Strategie definiert, mit welcher diese Einträge verwaltet werden können. Sobald er also setStrategy aufruft, werden alle bisherigen Einträge mit dieser Strategie behandelt.

    Grüssli



  • PhilippM schrieb:

    Okay, neuer Vorschlag:
    Wenn wir Problem 1 nicht lösen können, dann wenigstens Problem 2:
    Gibt es eine Möglichkeit dem Linker beim Zusammenbauen der dynamsichen Library zu sagen "Das excutyble, das diese lib später benutzen wird, bietet garantiert die Funktion XYZ an", so dass ich den alten Mechanismus mit dem undefiniert lassen und erst in der Applikation definieren, in die dynamische library rüberretten kann?

    Das geht, kann aber compilerabhängig Kunstgriffe erfordern. Wenn ich mich recht entsinne, muss bei Verwendung des gcc beispielsweise das Programm mit -Wl,-E kompiliert werden, damit die Executable eine dynamische Symboltabelle bekommt, die der Linker benutzen kann.

    Wie es bei anderen Compilern aussieht, kann ich dir aus dem Stand nicht sagen - ich hab sowas nie gebraucht.


Log in to reply