Ist es eine gute Praxis, Logger als Singleton zu haben?

80

Ich hatte die Angewohnheit, Logger an Konstruktor weiterzugeben, wie:

public class OrderService : IOrderService {
     public OrderService(ILogger logger) {
     }
}

Aber das ist ziemlich ärgerlich, deshalb habe ich es für einige Zeit als Eigenschaft benutzt:

private ILogger logger = NullLogger.Instance;
public ILogger Logger
{
    get { return logger; }
    set { logger = value; }
}

Das wird auch nervig - es ist nicht trocken, ich muss das in jeder Klasse wiederholen. Ich könnte die Basisklasse verwenden, aber andererseits - ich verwende die Form-Klasse, würde also FormBase usw. benötigen. Ich denke also, was wäre der Nachteil, wenn Singleton mit ILogger verfügbar gemacht würde, damit jeder weiß, wo er Logger bekommt:

    Infrastructure.Logger.Info("blabla");

UPDATE: Wie Merlyn richtig bemerkt hat, sollte ich erwähnen, dass ich im ersten und zweiten Beispiel DI verwende.

Giedrius
quelle
1
Aus den Antworten, die ich sehe, geht hervor, dass die Leute nicht bemerkt haben, dass Sie beide Beispiele injizieren . Können Sie dies in der Frage klarer machen? Ich habe Tags hinzugefügt, um einige davon zu erledigen.
Merlyn Morgan-Graham
1
Lesen Sie die Kommentare zu diesem Artikel Mythos der Abhängigkeitsinjektion: Referenzübergabe von Miško Hevery von Google, der sich mit Tests und Abhängigkeitsinjektion beschäftigt. Er sagt, dass die Protokollierung eine Art Ausnahme darstellt, bei der ein Singleton in Ordnung ist, es sei denn, Sie müssen die Ausgabe des Protokollierers testen (in diesem Fall verwenden Sie DI).
Benutzer

Antworten:

35

Das wird auch nervig - es ist nicht trocken

Das stimmt. Aber es gibt nur so viel, was Sie für ein Querschnittsthema tun können, das jeden Typ durchdringt, den Sie haben. Sie müssen verwenden Sie den Logger überall, so dass Sie die Eigenschaft auf diesen Typen haben müssen.

Mal sehen, was wir dagegen tun können.

Singleton

Singletons sind schrecklich <flame-suit-on>.

Ich empfehle, bei der Eigenschaftsinjektion zu bleiben, wie Sie es bei Ihrem zweiten Beispiel getan haben. Dies ist das beste Factoring, das Sie durchführen können, ohne auf Magie zurückzugreifen. Es ist besser, eine explizite Abhängigkeit zu haben, als sie über einen Singleton auszublenden.

Aber wenn Singletons Ihnen viel Zeit sparen, einschließlich aller Umgestaltungen, die Sie jemals durchführen müssen (Kristallkugelzeit!), Können Sie wahrscheinlich mit ihnen leben. Wenn es jemals eine Verwendung für einen Singleton gab, könnte dies der Fall sein. Denken Sie daran , dass die Kosten, wenn Sie jemals Ihre Meinung ändern möchten, so hoch wie möglich sind.

Wenn Sie dies tun, überprüfen Sie die Antworten anderer Personen anhand des RegistryMusters (siehe Beschreibung) und derjenigen, die eine (zurücksetzbare) Singleton- Factory anstelle einer Singleton-Logger-Instanz registrieren .

Es gibt andere Alternativen, die ohne Kompromisse genauso gut funktionieren könnten. Sie sollten sie daher zuerst überprüfen.

Visual Studio-Codefragmente

Sie können Visual Studio-Codefragmente verwenden , um die Eingabe dieses sich wiederholenden Codes zu beschleunigen. Sie können so etwas loggertabeingeben und der Code wird auf magische Weise für Sie angezeigt.

Mit AOP abtrocknen

Sie können ein wenig von diesem Eigenschaftsinjektionscode entfernen , indem Sie ein AOP-Framework (Aspect Oriented Programming) wie PostSharp verwenden, um einen Teil davon automatisch zu generieren.

Es könnte ungefähr so ​​aussehen, wenn Sie fertig sind:

[InjectedLogger]
public ILogger Logger { get; set; }

Sie können auch den Beispielcode für die Methodenverfolgung verwenden, um den Eingangs- und Ausgangscode für Methoden automatisch zu verfolgen, sodass möglicherweise nicht alle Logger-Eigenschaften zusammengefügt werden müssen. Sie können das Attribut auf Klassenebene oder im gesamten Namespace anwenden:

[Trace]
public class MyClass
{
    // ...
}

// or

#if DEBUG
[assembly: Trace( AttributeTargetTypes = "MyNamespace.*",
    AttributeTargetTypeAttributes = MulticastAttributes.Public,
    AttributeTargetMemberAttributes = MulticastAttributes.Public )]
#endif
Merlyn Morgan-Graham
quelle
3
+1 für Singletons sind schrecklich. Versuchen Sie, Singleton mit mehreren Klassenladeprogrammen zu verwenden, dh in einer Anwendungsserverumgebung, in der der Dispatcher der Client ist, in der ich schreckliche Beispiele für die doppelte Protokollierung erlebt habe (aber nicht so schrecklich wie die Protokollierung von Anweisungen, die von der falschen Stelle stammen, von der mir ein C ++ - Programmierer erzählt hat)
Niklas R.
1
@NickRosencrantz +1 für die C ++ - Horrorgeschichte; Ich liebe es, ein paar Minuten damit zu verbringen, über die Schrecklichkeit von Singletons nachzudenken, nur um nach unten zu scrollen und zu sagen: "Haha, zumindest habe ich ihre Probleme nicht."
Domenic
32
</flame-suit-on> Ich weiß nicht, wie Sie 5 Jahre in einem Flammenanzug gelebt haben, aber ich hoffe, das hilft.
Brennen Sprimont
39

Ich habe eine Logger-Instanz in meinen Abhängigkeitsinjektionscontainer eingefügt, die den Logger dann in die Klassen injiziert, die eine benötigen.

Daniel Rose
quelle
8
Könnten Sie genauer sein? Weil ich denke, dass ich das in 1 und 2 fraglichen Codebeispielen getan / getan habe?
Giedrius
2
Die "using" -Klasse würde im Grunde genauso aussehen wie das, was Sie bereits haben. Der tatsächliche Wert muss jedoch nicht manuell an Ihre Klasse übergeben werden, da der DI-Container dies für Sie erledigt. Dies macht es viel einfacher, den Logger in einem Test zu ersetzen, als einen statischen Singleton-Logger.
Daniel Rose
2
Nicht abstimmend, obwohl dies die richtige Antwort (IMO) ist, weil das OP bereits gesagt hat, dass sie dies tun. Was ist Ihre Begründung für die Verwendung dieser Funktion im Vergleich zur Verwendung eines Singleton? Was ist mit den Problemen, die das OP mit sich wiederholendem Code hat - wie wird das durch diese Antwort angegangen?
Merlyn Morgan-Graham
1
@ Merlyn Das OP gab an, dass er einen Logger als Teil des Konstruktors oder als öffentliches Eigentum hat. Er hat nicht angegeben, dass er einen DI-Container hat, der den richtigen Wert injiziert. Ich gehe also davon aus, dass dies nicht der Fall ist. Ja, es gibt einige wiederholte Codes (aber mit einer zugewiesenen Auto-Eigenschaft ist es zum Beispiel sehr wenig), aber meiner Meinung nach ist es viel besser, die Abhängigkeit explizit anzuzeigen, als sie über einen (globalen) Singleton auszublenden.
Daniel Rose
1
@ DanielRose: Und Sie haben meine Meinung geändert mit "Es ist viel besser, die Abhängigkeit explizit anzuzeigen, als sie über einen (globalen) Singleton zu verbergen". +1
Merlyn Morgan-Graham
25

Gute Frage. Ich glaube, in den meisten Projekten ist Logger ein Singleton.

Einige Ideen kommen mir gerade in den Sinn:

  • Verwenden Sie ServiceLocator (oder einen anderen Dependency Injection- Container, falls Sie bereits einen verwenden), mit dem Sie den Logger für alle Dienste / Klassen freigeben können. Auf diese Weise können Sie den Logger oder sogar mehrere verschiedene Logger instanziieren und über ServiceLocator freigeben, was offensichtlich ein Singleton wäre , eine Art Umkehrung der Kontrolle . Dieser Ansatz bietet Ihnen viel Flexibilität bei der Instanziierung und Initialisierung von Loggern.
  • Wenn Sie fast überall Logger - Erweiterungsmethoden für implementieren ObjectTyp so würde jede Klasse in der Lage sein mag Loggers Methoden aufrufen LogInfo(), LogDebug(),LogError()
sll
quelle
Könnten Sie genauer sagen, was Sie über Erweiterungsmethoden gedacht haben? In Bezug auf die Verwendung von ServiceLocator frage ich mich, warum es besser wäre als eine Eigenschaftsinjektion, wie in Beispiel 2.
Giedrius
1
@Giedrius: In Bezug auf Erweiterungsmethoden - Sie können Erweiterungsmethoden erstellen, public static void LogInfo(this Object instance, string message)so dass jede Klasse sie aufgreifen würde. In Bezug auf ServiceLocator- dies ermöglicht es Ihnen, Logger als reguläre Klasseninstanz und nicht als Singleton zu verwenden, sodass Sie viel Flexibilität geben würden
sll
@Giedrius Wenn Sie IoC durch Konstruktorinjektion implementieren und das von Ihnen verwendete DI-Framework Ihre Konstruktoren scannen und Ihre Abhängigkeiten automatisch injizieren kann (vorausgesetzt, Sie haben diese Abhängigkeiten für Ihren DI-Framework-Bootstrap-Prozess konfiguriert), dann wie früher tun würde es gut funktionieren. Außerdem sollten die meisten DI-Frameworks es Ihnen ermöglichen, den Lebenszyklusbereich jedes Objekts festzulegen.
GR7
7
ServiceLocator ist kein DI-Container. Und es gilt als Anti-Muster.
Piotr Perak
1
Das OP verwendet DI bereits mit einem DI-Container. Das erste Beispiel ist die CTOR-Injektion, das zweite ist die Eigenschaftsinjektion. Siehe ihre Bearbeitung.
Merlyn Morgan-Graham
16

Ein Singleton ist eine gute Idee. Eine noch bessere Idee ist die Verwendung des Registrierungsmusters , mit dem die Instanziierung etwas besser gesteuert werden kann. Meiner Meinung nach liegt das Singleton-Muster zu nahe an globalen Variablen. Bei einer Registrierung, die die Objekterstellung oder -wiederverwendung verwaltet, ist Platz für zukünftige Änderungen der Instanziierungsregeln.

Die Registrierung selbst kann eine statische Klasse sein, um eine einfache Syntax für den Zugriff auf das Protokoll bereitzustellen:

Registry.Logger.Info("blabla");
Anders Abel
quelle
6
Zum ersten Mal bin ich auf das Registrierungsmuster gestoßen. Ist es eine zu starke Vereinfachung zu sagen, dass im Grunde alle globalen Variablen und Funktionen unter einem Dach zusammengefasst sind?
Joel Goodwin
In seiner einfachsten Form ist es nur ein Regenschirm, aber wenn es an einem zentralen Ort ist, kann es in etwas anderes geändert werden (möglicherweise in einen Pool von Objekten, Instanz pro Verwendung, threadlokale Instanz), wenn sich die Anforderungen ändern.
Anders Abel
5
Registry und ServiceLocator sind ziemlich identisch. Die meisten IoC-Frameworks sind im Kern eine Implementierung der Registrierung.
Michael Brown
1
@MikeBrown: Der Unterschied scheint darin zu liegen, dass ein DI-Container (intern Service Locator-Muster) eher eine instanziierte Klasse ist, während das Registrierungsmuster eine statische Klasse verwendet. Klingt das richtig?
Merlyn Morgan-Graham
1
@MichaelBrown Der Unterschied besteht darin, wo Sie den Registrierungs- / Service-Locator verwenden. Wenn es nur in der Kompositionswurzel ist, ist es in Ordnung; Wenn Sie an vielen Orten verwenden, ist es schlecht.
Onur
10

Ein einfacher Singleton ist keine gute Idee. Es macht es schwierig, den Logger auszutauschen. Ich neige dazu, Filter für meine Logger zu verwenden (einige "verrauschte" Klassen protokollieren möglicherweise nur Warnungen / Fehler).

Ich verwende ein Singleton-Muster in Kombination mit dem Proxy-Muster für die Logger-Factory:

public class LogFactory
{
    private static LogFactory _instance;

    public static void Assign(LogFactory instance)
    {
        _instance = instance;
    }

    public static LogFactory Instance
    {
        get { _instance ?? (_instance = new LogFactory()); }
    }

    public virtual ILogger GetLogger<T>()
    {
        return new SystemDebugLogger();
    }
}

Dies ermöglicht es mir, einen FilteringLogFactoryoder nur einen zu erstellen, SimpleFileLogFactoryohne Code zu ändern (und daher dem Open / Closed-Prinzip zu entsprechen).

Beispielerweiterung

public class FilteredLogFactory : LogFactory
{
    public override ILogger GetLogger<T>()
    {
        if (typeof(ITextParser).IsAssignableFrom(typeof(T)))
            return new FilteredLogger(typeof(T));

        return new FileLogger(@"C:\Logs\MyApp.log");
    }
}

Und um die neue Fabrik zu nutzen

// and to use the new log factory (somewhere early in the application):
LogFactory.Assign(new FilteredLogFactory());

In Ihrer Klasse sollte das protokollieren:

public class MyUserService : IUserService
{
    ILogger _logger = LogFactory.Instance.GetLogger<MyUserService>();

    public void SomeMethod()
    {
        _logger.Debug("Welcome world!");
    }
}
jgauffin
quelle
Vergiss meinen vorherigen Kommentar. Ich glaube ich sehe was du hier machst. Können Sie ein Beispiel für eine Klasse angeben, die sich tatsächlich bei dieser Klasse anmeldet?
Merlyn Morgan-Graham
+1; Schlägt definitiv einen nackten Singleton :) Hat aufgrund der Verwendung statischer Objekte immer noch leichte Kopplungen und das Potenzial für Probleme mit dem statischen Bereich / Threading, aber ich stelle mir vor, dass diese selten sind (Sie möchten sie Instanceim Anwendungsstamm festlegen und ich weiß es nicht warum Sie es zurücksetzen würden)
Merlyn Morgan-Graham
1
@ MerlynMorgan-Graham: Es ist unwahrscheinlich, dass Sie eine Fabrik ändern, nachdem Sie sie eingestellt haben. Alle Änderungen werden in der implementierenden Factory vorgenommen, und Sie haben die volle Kontrolle darüber. Ich würde dieses Muster nicht als universelle Lösung für Singletons empfehlen, aber es funktioniert gut für Fabriken, da sich ihre API selten ändert. (so könnte man es als Proxy abstrakte Fabrik Singleton, hehe, Muster-Mash-up bezeichnen)
jgauffin
3

In .NET gibt es ein Buch Dependency Injection. Je nachdem, was Sie benötigen, sollten Sie das Abfangen verwenden.

In diesem Buch gibt es ein Diagramm, das bei der Entscheidung hilft, ob Konstruktorinjektion, Eigenschaftsinjektion, Methodeninjektion, Umgebungskontext und Abfangen verwendet werden sollen.

So begründet man die Verwendung dieses Diagramms:

  1. Haben Sie Abhängigkeit oder brauchen Sie diese? - Brauchen
  2. Ist es ein Querschnittsthema? - Ja
  3. Benötigen Sie eine Antwort darauf? - Nein

Verwenden Sie Interception

Piotr Perak
quelle
Ich denke, es gibt einen Fehler in der ersten Argumentationsfrage (ich denke, es sollte "Haben Sie Abhängigkeit oder brauchen Sie sie?" Sein). Wie auch immer, +1 für die Erwähnung von Interception, das Studium der Möglichkeiten in Windsor und es sieht sehr interessant aus. Wenn Sie zusätzlich ein Beispiel geben könnten, wie Sie sich vorstellen, dass es hier passen würde, wäre es großartig.
Giedrius
+1; Interessanter Vorschlag - würde im Grunde AOP-ähnliche Funktionalität geben, ohne die Typen berühren zu müssen. Meine Meinung (die auf einer sehr begrenzten Belichtung basiert ) ist, dass sich das Abfangen über die Proxy-Generierung (der Typ, den eine DI-Bibliothek im Gegensatz zu einer auf AOP-Attributen basierenden Bibliothek bereitstellen kann) wie schwarze Magie anfühlt und es etwas schwierig machen kann, zu verstehen, was ist los. Ist mein Verständnis, wie das funktioniert, falsch? Hat jemand eine andere Erfahrung damit gemacht? Ist es weniger beängstigend als es klingt?
Merlyn Morgan-Graham
2
Wenn Sie der Meinung sind, dass Abfangen zu viel „Magie“ ist, können Sie einfach das Dekorationsmuster verwenden und es selbst implementieren. Aber danach werden Sie wahrscheinlich feststellen, dass es Zeitverschwendung ist.
Piotr Perak
Ich ging davon aus, dass dieser Vorschlag jeden Aufruf automatisch in die Protokollierung einbinden würde, anstatt das Klassenprotokoll selbst erstellen zu lassen. Ist das korrekt?
Merlyn Morgan-Graham
Ja. Das macht der Dekorateur.
Piotr Perak
0

Eine andere Lösung, die ich persönlich am einfachsten finde, ist die Verwendung einer statischen Logger-Klasse. Sie können es von jeder Klassenmethode aus aufrufen, ohne die Klasse ändern zu müssen, z. B. Eigenschaftsinjektion hinzufügen usw. Es ist ziemlich einfach und benutzerfreundlich.

Logger::initialize ("filename.log", Logger::LEVEL_ERROR); // only need to be called once in your application

Logger::log ("my error message", Logger::LEVEL_ERROR); // to be used in every method where needed
Erik Kalkoken
quelle
-1

Wenn Sie sich eine gute Lösung für die Protokollierung ansehen möchten, empfehlen wir Ihnen, sich die Google App Engine mit Python anzusehen, bei der die Protokollierung so einfach wie möglich ist. import loggingDann können Sie sie einfach logging.debug("my message")oder logging.info("my message")so einfach halten, wie sie sollte.

Java hatte keine gute Lösung für die Protokollierung, dh log4j sollte vermieden werden, da es Sie praktisch dazu zwingt, Singletons zu verwenden, was, wie hier beantwortet, "schrecklich" ist, und ich habe schreckliche Erfahrungen damit gemacht, die Protokollausgabe nur einmal mit derselben Protokollierungsanweisung zu versehen wenn ich vermute, dass der Grund für die doppelte Protokollierung darin bestand, dass ich einen Singleton des Protokollierungsobjekts in zwei Klassenladeprogrammen in derselben virtuellen Maschine (!) habe.

Ich bitte um Verzeihung, dass Sie nicht so spezifisch für C # sind, aber nach dem, was ich gesehen habe, sehen die Lösungen mit C # ähnlich aus wie Java, wo wir log4j hatten, und wir sollten es auch zu einem Singleton machen.

Aus diesem Grund hat mir die Lösung mit GAE / Python sehr gut gefallen. Sie ist so einfach wie möglich und Sie müssen sich keine Gedanken über Klassenladeprogramme, doppelte Protokollierungsanweisungen oder Designmuster machen.

Ich hoffe, dass einige dieser Informationen für Sie relevant sein können, und ich hoffe, dass Sie einen Blick auf die von mir empfohlene Protokollierungslösung werfen möchten. Stattdessen schikaniere ich, wie viel Problem Singleton aufgrund der Unmöglichkeit, wann einen echten Singleton zu haben, vermutet wird es muss in mehreren Klassenladern instanziiert werden.

Niklas R.
quelle
Ist der äquivalente Code in C # nicht ein Singleton (oder eine statische Klasse, die möglicherweise gleich ist, möglicherweise schlechter, da sie noch weniger Flexibilität bietet)? using Logging; /* ... */ Logging.Info("my message");
Merlyn Morgan-Graham
Und Sie können Singletons mit dem log4j-Muster vermeiden, wenn Sie die Abhängigkeitsinjektion verwenden, um Ihre Logger zu injizieren. Viele Bibliotheken tun dies. Sie können auch Wrapper verwenden, die generische Schnittstellen bereitstellen, damit Sie Ihre Protokollierungsimplementierung zu einem späteren Zeitpunkt austauschen können , z. B. netcommon.sourceforge.net oder eine der Erweiterungen, die von DI-Containerbibliotheken wie Castle.Windsor oder Ninject
Merlyn Morgan-Graham
Das ist das Problem! Niemand verwendet jemals logging.info("my message")in einem Programm, das komplizierter ist als die Hallo Welt. Normalerweise führen Sie eine ganze Reihe von Boilerplates durch, um den Logger zu initialisieren. Sie richten den Formatierer und die Ebene ein und richten sowohl Datei- als auch Konsolenhandler ein. Es ist nie jemals logging.info("my message")!
Der Pate