Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Stellvertretungen bieten einen Mechanismus, mit dem Softwaredesigns mit minimaler Kopplung zwischen Komponenten ermöglicht werden.
Ein hervorragendes Beispiel für diese Art von Design ist LINQ. Das LINQ-Abfrageausdrucksmuster stützt sich auf Delegaten für alle Funktionen. Betrachten Sie dieses einfache Beispiel:
var smallNumbers = numbers.Where(n => n < 10);
Dadurch wird die Reihenfolge von Zahlen nur auf diejenigen gefiltert, die kleiner als der Wert 10 sind.
Die Where
Methode verwendet einen Delegaten, der bestimmt, welche Elemente einer Sequenz den Filter übergeben. Wenn Sie eine LINQ-Abfrage erstellen, geben Sie die Implementierung des Delegaten für diesen bestimmten Zweck an.
Der Prototyp für die Where-Methode lautet:
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);
Dieses Beispiel wird mit allen Methoden wiederholt, die Teil von LINQ sind. Sie alle verlassen sich auf Stellvertretungen für den Code, der die spezifische Abfrage verwaltet. Dieses API-Entwurfsmuster ist ein leistungsfähiges Muster, um zu lernen und zu verstehen.
In diesem einfachen Beispiel wird veranschaulicht, wie Delegaten nur wenig Kopplung zwischen Komponenten erfordern. Sie müssen keine Klasse erstellen, die von einer bestimmten Basisklasse abgeleitet wird. Sie müssen keine bestimmte Schnittstelle implementieren. Die einzige Anforderung besteht darin, die Implementierung einer Methode bereitzustellen, die für die Aufgabe von grundlegender Bedeutung ist.
Eigene Komponenten mit Delegierten erstellen
Wir bauen auf diesem Beispiel auf, indem wir eine Komponente mithilfe eines Designs erstellen, das auf Stellvertretungen basiert.
Definieren wir eine Komponente, die für Protokollnachrichten in einem großen System verwendet werden kann. Die Bibliothekskomponenten können in vielen verschiedenen Umgebungen auf mehreren verschiedenen Plattformen verwendet werden. Es gibt viele allgemeine Features in der Komponente, die die Protokolle verwaltet. Es muss Nachrichten von jeder Komponente im System akzeptieren. Diese Nachrichten haben unterschiedliche Prioritäten, die die Kernkomponente verwalten kann. Die Nachrichten sollten in ihrem endgültig archivierten Format Zeitstempel enthalten. Für komplexere Szenarien können Sie Nachrichten nach der Quellkomponente filtern.
Es gibt einen Aspekt des Features, der sich häufig ändert: Wo Nachrichten geschrieben werden. In einigen Umgebungen werden sie möglicherweise in die Fehlerkonsole geschrieben. In anderen Fällen eine Datei. Weitere Möglichkeiten sind Datenbankspeicher, Betriebssystemereignisprotokolle oder andere Dokumentspeicher.
Es gibt auch Kombinationen von Ausgaben, die in verschiedenen Szenarien verwendet werden können. Möglicherweise möchten Sie Nachrichten in die Konsole und in eine Datei schreiben.
Ein Design, das auf Stellvertretungen basiert, bietet eine große Flexibilität und erleichtert die Unterstützung von Speichermechanismen, die in Zukunft hinzugefügt werden können.
Unter diesem Entwurf kann es sich bei der primären Protokollkomponente um eine nicht virtuelle, sogar versiegelte Klasse handeln. Sie können jeden Satz von Delegierten verwenden, um die Nachrichten in verschiedene Speichermedien zu schreiben. Die integrierte Unterstützung für Multicastdelegats erleichtert die Unterstützung von Szenarien, in denen Nachrichten an mehrere Speicherorte geschrieben werden müssen (eine Datei und eine Konsole).
Erste Implementierung
Fangen wir klein an: Die anfängliche Implementierung akzeptiert neue Meldungen, und schreibt mithilfe von angefügten Delegaten. Sie können mit einem Delegaten beginnen, der Nachrichten in die Konsole schreibt.
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(string msg)
{
if (WriteMessage is not null)
WriteMessage(msg);
}
}
Die obige statische Klasse ist das einfachste, was funktionieren kann. Wir müssen die einzelne Implementierung für die Methode schreiben, die Nachrichten in die Konsole schreibt:
public static class LoggingMethods
{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}
Abschließend müssen Sie den Delegaten verknüpfen, indem Sie ihn an den WriteMessage-Delegaten anfügen, der in der Protokollierung deklariert wurde:
Logger.WriteMessage += LoggingMethods.LogToConsole;
Praktiken
Unser Beispiel ist bisher ziemlich einfach, zeigt aber dennoch einige der wichtigen Richtlinien für Entwürfe mit Delegierten.
Die Verwendung der im Kernframework definierten Delegattypen erleichtert benutzern das Arbeiten mit den Delegaten. Sie müssen keine neuen Typen definieren, und Entwickler, die Ihre Bibliothek verwenden, müssen keine neuen, spezialisierten Delegattypen erlernen.
Die verwendeten Schnittstellen sind so minimal und flexibel wie möglich: Um einen neuen Ausgabeprotokollierer zu erstellen, müssen Sie eine Methode erstellen. Diese Methode kann eine statische Methode oder eine Instanzmethode sein. Es kann über jeden Zugriff verfügen.
Formatieren der Ausgabe
Lassen Sie uns diese erste Version etwas robuster machen und dann mit dem Erstellen anderer Protokollierungsmechanismen beginnen.
Als Nächstes fügen wir der Methode ein paar Argumente hinzu LogMessage()
, damit Ihre Protokollklasse strukturiertere Nachrichten erstellt:
public enum Severity
{
Verbose,
Trace,
Information,
Warning,
Error,
Critical
}
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(Severity s, string component, string msg)
{
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
if (WriteMessage is not null)
WriteMessage(outputMsg);
}
}
Als Nächstes verwenden wir dieses Severity
-Argument, um die Meldungen zu filtern, die in das Ausgabeprotokoll gesendet werden.
public static class Logger
{
public static Action<string>? WriteMessage;
public static Severity LogLevel { get; set; } = Severity.Warning;
public static void LogMessage(Severity s, string component, string msg)
{
if (s < LogLevel)
return;
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
if (WriteMessage is not null)
WriteMessage(outputMsg);
}
}
Praktiken
Sie haben der Protokollierungsinfrastruktur neue Features hinzugefügt. Da die Loggerkomponente sehr lose mit jedem Ausgabemechanismus gekoppelt ist, können diese neuen Features ohne Auswirkungen auf den Code hinzugefügt werden, der den Loggerdelegat implementiert.
Während Sie dies weiter erstellen, sehen Sie weitere Beispiele dafür, wie diese lose Kopplung mehr Flexibilität beim Aktualisieren von Teilen der Website ohne Änderungen an anderen Standorten ermöglicht. In einer größeren Anwendung sind die Loggerausgabeklassen möglicherweise in einer anderen Assembly enthalten und müssen nicht einmal neu erstellt werden.
Erstellen einer zweiten Ausgabe-Engine
Die Log-Komponente macht gute Fortschritte. Fügen wir ein weiteres Ausgabemodul hinzu, das Nachrichten in einer Datei protokolliert. Dies wird eine etwas komplexere Ausgabe-Engine werden. Es handelt sich um eine Klasse, die die Dateivorgänge kapselt, und stellt sicher, dass die Datei nach jedem Schreibvorgang immer geschlossen wird. Dadurch wird sichergestellt, dass alle Daten nach dem Generieren jeder Nachricht auf den Datenträger geleert werden.
Dies ist der dateibasierte Logger:
public class FileLogger
{
private readonly string logPath;
public FileLogger(string path)
{
logPath = path;
Logger.WriteMessage += LogMessage;
}
public void DetachLog() => Logger.WriteMessage -= LogMessage;
// make sure this can't throw.
private void LogMessage(string msg)
{
try
{
using (var log = File.AppendText(logPath))
{
log.WriteLine(msg);
log.Flush();
}
}
catch (Exception)
{
// Hmm. We caught an exception while
// logging. We can't really log the
// problem (since it's the log that's failing).
// So, while normally, catching an exception
// and doing nothing isn't wise, it's really the
// only reasonable option here.
}
}
}
Nachdem Sie diese Klasse erstellt haben, können Sie sie instanziieren und die LogMessage-Methode an die Logger-Komponente anfügen:
var file = new FileLogger("log.txt");
Diese beiden schließen sich nicht gegenseitig aus. Sie können beide Protokollmethoden anfügen und Nachrichten an die Konsole und eine Datei generieren:
var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier
Später können Sie in derselben Anwendung einen der Delegierten entfernen, ohne dass dadurch andere Probleme im System auftreten.
Logger.WriteMessage -= LoggingMethods.LogToConsole;
Praktiken
Nun haben Sie einen zweiten Ausgabehandler für das Protokollierungssubsystem hinzugefügt. Dies erfordert etwas mehr Infrastruktur, um das Dateisystem korrekt zu unterstützen. Bei dem Delegat handelt es sich um eine Instanzmethode. Es ist auch eine private Methode. Es besteht keine Notwendigkeit, die Zugänglichkeit zu verbessern, da die Delegierten-Infrastruktur die Delegierten verbinden kann.
Zweitens ermöglicht das delegatbasierte Design mehrere Ausgabemethoden ohne zusätzlichen Code. Sie müssen keine zusätzliche Infrastruktur erstellen, um mehrere Ausgabemethoden zu unterstützen. Sie erhalten einfach eine andere Methode auf der Aufrufliste.
Achten Sie besonders auf den Code in der Ausgabemethode für die Dateiprotokollierung. Es ist codiert, um sicherzustellen, dass keine Ausnahmen ausgelöst werden. Obwohl dies nicht immer unbedingt notwendig ist, ist es oft eine gute Methode. Wenn eine der Delegatmethoden eine Ausnahme auslöst, werden die im Aufruf verbleibenden Delegaten nicht aufgerufen werden.
Als letzte Notiz muss der Dateiprotokollierer seine Ressourcen verwalten, indem die Datei in jeder Protokollnachricht geöffnet und geschlossen wird. Sie könnten sich dafür entscheiden, die Datei offen zu halten und IDisposable
verwenden, um die Datei zu schließen, wenn Sie fertig sind.
Beide Methoden haben ihre Vor- und Nachteile. Beide schaffen etwas mehr Kopplung zwischen den Klassen.
Der Code in der Logger
Klasse muss nicht aktualisiert werden, um beide Szenarien zu unterstützen.
Verarbeiten von NULL-Delegaten
Abschließend aktualisieren wir die LogMessage-Methode so, dass sie für diese Fälle robust ist, wenn kein Ausgabemechanismus ausgewählt ist. Die aktuelle Implementierung löst eine NullReferenceException
aus, wenn der WriteMessage
-Delegat nicht über eine angefügte Aufrufliste verfügt.
Möglicherweise bevorzugen Sie ein Design, das stumm fortgesetzt wird, wenn keine Methoden angehängt wurden. Dies ist einfach mit dem bedingten Operator null, kombiniert mit der Delegate.Invoke()
Methode:
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
Der bedingte NULL-Operator (?.
) wird verkürzt, wenn der linke Operand (WriteMessage
in diesem Fall) NULL ist, was bedeutet, dass nicht versucht wurde, eine Meldung zu protokollieren.
Sie werden die Methode Invoke()
nicht in der Dokumentation von System.Delegate
oder System.MulticastDelegate
finden. Der Compiler generiert eine typsichere Invoke
Methode für jeden deklarierten Delegatentyp. In diesem Beispiel bedeutet das, dass Invoke
ein einzelnes string
-Argument nimmt und über einen void-Rückgabetyp verfügt.
Zusammenfassung der Praktiken
Sie haben die Anfänge einer Protokollkomponente gesehen, die mit anderen Autoren und anderen Features erweitert werden kann. Durch die Verwendung von Delegates im Design sind diese verschiedenen Komponenten lose verbunden. Dies bietet mehrere Vorteile. Es ist einfach, neue Ausgabemechanismen zu erstellen und an das System anzufügen. Diese anderen Mechanismen benötigen nur eine Methode: die Methode, die die Protokollnachricht schreibt. Es handelt sich um ein Design, das robust ist, wenn neue Features hinzugefügt werden. Der Vertrag, der für jeden Autor erforderlich ist, besteht darin, eine Methode zu implementieren. Diese Methode kann eine statische oder Instanzmethode sein. Es kann sich um einen öffentlichen, privaten oder jeden anderen rechtlichen Zugriff handeln.
Die Protokollierungsklasse kann eine beliebige Anzahl von Verbesserungen oder Änderungen vornehmen, ohne wichtige Änderungen einzuführen. Wie jede Klasse können Sie die öffentliche API nicht ohne das Risiko von wichtigen Änderungen ändern. Da die Kopplung zwischen dem Logger und allen Ausgabemodulen jedoch nur über den Delegaten erfolgt, sind keine anderen Typen (z. B. Schnittstellen oder Basisklassen) beteiligt. Die Kupplung ist so klein wie möglich.