Condividi tramite


Modelli comuni per i delegati

Precedente

I delegati forniscono un meccanismo che consente la progettazione software che implica un accoppiamento minimo tra i componenti.

Un esempio eccellente per questo tipo di progettazione è LINQ. Il modello di espressione di query LINQ si basa su delegati per tutte le relative funzionalità. Si consideri questo semplice esempio:

var smallNumbers = numbers.Where(n => n < 10);

In questo modo viene filtrata la sequenza di numeri in modo che siano inferiori al valore 10. Il Where metodo usa un delegato che determina quali elementi di una sequenza passano il filtro. Quando si crea una query LINQ, si fornisce l'implementazione del delegato per questo scopo specifico.

Il prototipo per il metodo Where è:

public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);

Questo esempio viene ripetuto con tutti i metodi che fanno parte di LINQ. Tutti si basano sui delegati per il codice che gestisce la query specifica. Questo modello di progettazione DELL'API è un potente strumento per imparare e comprendere.

Questo semplice esempio illustra come i delegati richiedono un accoppiamento molto minimo tra i componenti. Non è necessario creare una classe che deriva da una determinata classe di base. Non è necessario implementare un'interfaccia specifica. L'unico requisito consiste nel fornire l'implementazione di un metodo fondamentale per l'attività.

Creare componenti personalizzati con delegati

Costruiamo su questo esempio creando un componente usando una progettazione che si basa su delegati.

Definire un componente che può essere usato per registrare i messaggi in un sistema di grandi dimensioni. I componenti della libreria possono essere usati in molti ambienti diversi, su più piattaforme diverse. Esistono molte funzionalità comuni nel componente che gestisce i log. Dovrà accettare messaggi da qualsiasi componente del sistema. Questi messaggi avranno priorità diverse, che il componente principale può gestire. I messaggi devono avere timestamp nel formato archiviato finale. Per scenari più avanzati, è possibile filtrare i messaggi in base al componente di origine.

C'è un aspetto della funzionalità che cambierà spesso: dove vengono scritti i messaggi. In alcuni ambienti possono essere scritti nella console degli errori. In altri casi, un file. Altre possibilità includono l'archiviazione del database, i registri eventi del sistema operativo o altre risorse di archiviazione dei documenti.

Esistono anche combinazioni di output che possono essere usate in scenari diversi. È possibile scrivere messaggi nella console e in un file.

Una progettazione basata su delegati fornirà una grande flessibilità e semplifica il supporto dei meccanismi di archiviazione che potrebbero essere aggiunti in futuro.

In questa progettazione, il componente del log primario può essere una classe non virtuale, anche sigillata. È possibile collegare qualsiasi set di delegati per scrivere i messaggi in supporti di archiviazione diversi. Il supporto predefinito per i delegati multicast semplifica il supporto di scenari in cui i messaggi devono essere scritti in più posizioni (un file e una console).

Una prima implementazione

Iniziamo da piccolo: l'implementazione iniziale accetterà nuovi messaggi e li scriverà usando qualsiasi delegato allegato. È possibile iniziare con un delegato che scrive messaggi nella console.

public static class Logger
{
    public static Action<string>? WriteMessage;

    public static void LogMessage(string msg)
    {
        if (WriteMessage is not null)
            WriteMessage(msg);
    }
}

La classe statica precedente è la cosa più semplice che può funzionare. È necessario scrivere la singola implementazione per il metodo che scrive i messaggi nella console:

public static class LoggingMethods
{
    public static void LogToConsole(string message)
    {
        Console.Error.WriteLine(message);
    }
}

Infine, è necessario associare il delegato collegandolo al delegato WriteMessage dichiarato nel logger:

Logger.WriteMessage += LoggingMethods.LogToConsole;

Pratiche

Il nostro esempio finora è piuttosto semplice, ma dimostra ancora alcune delle linee guida importanti per le progettazioni che coinvolgono i delegati.

L'uso dei tipi delegati definiti nel framework principale semplifica l'uso dei delegati da parte degli utenti. Non è necessario definire nuovi tipi e gli sviluppatori che usano la libreria non devono apprendere nuovi tipi delegati specializzati.

Le interfacce usate sono quanto più minimali e flessibili possibile: per creare un nuovo logger di output, è necessario creare un metodo. Tale metodo può essere un metodo statico o un metodo di istanza. Può avere qualsiasi accesso.

Formato dell'output

Rendere questa prima versione un po' più affidabile e quindi iniziare a creare altri meccanismi di registrazione.

Aggiungere quindi alcuni argomenti al LogMessage() metodo in modo che la classe di log crei messaggi più strutturati:

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);
    }
}

Ora, utilizziamo l'argomento Severity per filtrare i messaggi che vengono inviati all'output del log.

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);
    }
}

Pratiche

Sono state aggiunte nuove funzionalità all'infrastruttura di registrazione. Poiché il componente logger è molto debolmente accoppiato a qualsiasi meccanismo di output, queste nuove funzionalità possono essere aggiunte senza alcun impatto sul codice che implementa il delegato del logger.

Man mano che si continua a costruire questo, verranno visualizzati altri esempi di come questo accoppiamento libero consenta una maggiore flessibilità nell'aggiornamento di parti del sito senza modifiche ad altre parti. Infatti, in un'applicazione più grande, le classi di output del logger potrebbero trovarsi in un assembly diverso e non devono nemmeno essere ricompilate.

Creare un secondo motore di output

Il componente Log sta procedendo bene. Aggiungiamo un motore di output aggiuntivo che salva i messaggi su un file. Questo sarà un motore di output leggermente più complesso. Sarà una classe che incapsula le operazioni di file e garantisce che il file venga sempre chiuso dopo ogni scrittura. Ciò garantisce che tutti i dati vengano scaricati su disco dopo la generazione di ogni messaggio.

Ecco il logger basato su file:

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.
        }
    }
}

Dopo aver creato questa classe, è possibile crearne un'istanza e allegare il relativo metodo LogMessage al componente Logger:

var file = new FileLogger("log.txt");

Questi due non si escludono a vicenda. È possibile allegare entrambi i metodi di log e generare messaggi alla console e a un file:

var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier

In un secondo momento, anche nella stessa applicazione, è possibile rimuovere uno dei delegati senza altri problemi al sistema:

Logger.WriteMessage -= LoggingMethods.LogToConsole;

Pratiche

A questo punto, è stato aggiunto un secondo gestore di output per il sottosistema di registrazione. Per supportare correttamente il file system, è necessario un po' più di infrastruttura. Il delegato è un metodo di istanza. È anche un metodo privato. Non è necessaria una maggiore accessibilità perché l'infrastruttura del delegato può connettere i delegati.

In secondo luogo, la progettazione basata su delegato abilita più metodi di output senza codice aggiuntivo. Non è necessario creare un'infrastruttura aggiuntiva per supportare più metodi di output. Semplicemente diventano un altro metodo nell'elenco di invocazioni.

Prestare particolare attenzione al codice nel metodo di output di registrazione file. Viene codificato per assicurarsi che non generi eccezioni. Anche se questo non è sempre strettamente necessario, è spesso una buona pratica. Se uno dei metodi delegati genera un'eccezione, i delegati rimanenti che si trovano nella chiamata non verranno richiamati.

Come ultima nota, il logger di file deve gestire le relative risorse aprendo e chiudendo il file in ogni messaggio di log. È possibile scegliere di mantenere aperto il file e implementare IDisposable per chiudere il file al termine. Entrambi i metodi presentano vantaggi e svantaggi. Entrambi creano un accoppiamento maggiore tra le classi.

Nessuno dei codici nella Logger classe deve essere aggiornato per supportare entrambi gli scenari.

Gestire i delegati Null

Infine, aggiorniamo il metodo LogMessage in modo che sia affidabile per questi casi quando non è selezionato alcun meccanismo di output. L'implementazione corrente genererà un'eccezione NullReferenceException quando il WriteMessage delegato non dispone di un elenco chiamate associato. È possibile preferire una progettazione che continua in modo invisibile all'utente quando non sono stati associati metodi. È facile usare l'operatore condizionale nullo, combinato con il metodo Delegate.Invoke().

public static void LogMessage(string msg)
{
    WriteMessage?.Invoke(msg);
}

L'operatore condizionale null (?.) si interrompe quando l'operando sinistro (WriteMessage in questo caso) è null, il che significa che non viene effettuato alcun tentativo di registrare un messaggio.

Non è possibile trovare il Invoke() metodo elencato nella documentazione per System.Delegate o System.MulticastDelegate. Il compilatore genera un metodo sicuro di tipo Invoke per qualsiasi tipo di delegato dichiarato. In questo esempio, ciò significa che Invoke accetta un singolo argomento string e ha un tipo di ritorno void.

Riepilogo delle procedure

Si sono visti gli inizi di un componente di registrazione che potrebbe essere ampliato con altri scrittori e altre funzionalità. Usando i delegati nella progettazione, questi diversi componenti sono ad accoppiamento libero. Ciò offre diversi vantaggi. È facile creare nuovi meccanismi di output e collegarli al sistema. Questi altri meccanismi richiedono un solo metodo: il metodo che scrive il messaggio di log. Si tratta di una progettazione resiliente quando vengono aggiunte nuove funzionalità. Il contratto richiesto per qualsiasi scrittore consiste nell'implementare un metodo. Questo metodo può essere un metodo statico o di istanza. Potrebbe essere pubblico, privato o qualsiasi altro accesso legale.

La classe Logger può apportare un numero qualsiasi di miglioramenti o modifiche senza introdurre modifiche di rilievo. Come qualsiasi classe, non è possibile modificare l'API pubblica senza il rischio di modifiche significative. Tuttavia, poiché l'accoppiamento tra il logger e qualsiasi motore di output avviene solo tramite il delegato, non sono coinvolti altri tipi (ad esempio interfacce o classi di base). L'accoppiamento è il più piccolo possibile.

Avanti