Běžné vzory pro delegáty

Předchozí

Delegáti poskytují mechanismus, který umožňuje návrhy softwaru zahrnující minimální párování mezi komponentami.

Skvělým příkladem tohoto druhu návrhu je LINQ. Vzor výrazu dotazu LINQ spoléhá na delegáty pro všechny jeho funkce. Představte si tento jednoduchý příklad:

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

Tím se posloupnost čísel vyfiltruje jenom na ty, které jsou menší než hodnota 10. Metoda Where používá delegáta, který určuje, které prvky sekvence předá filtr. Při vytváření dotazu LINQ zadáte implementaci delegáta pro tento konkrétní účel.

Prototyp metody Where je:

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

Tento příklad se opakuje se všemi metodami, které jsou součástí LINQ. Všichni spoléhají na delegáty pro kód, který spravuje konkrétní dotaz. Tento vzor návrhu rozhraní API je výkonný model, který se učí a rozumí.

Tento jednoduchý příklad ukazuje, jak delegáti vyžadují velmi malé párování mezi komponentami. Není nutné vytvářet třídu, která je odvozena z konkrétní základní třídy. Nemusíte implementovat konkrétní rozhraní. Jediným požadavkem je implementace jedné metody, která je pro úlohu zásadní.

Vytváření vlastních komponent s delegáty

Pojďme na tento příklad stavět vytvořením komponenty pomocí návrhu, který spoléhá na delegáty.

Pojďme definovat komponentu, která by se mohla používat pro zprávy protokolu ve velkém systému. Komponenty knihovny lze použít v mnoha různých prostředích na několika různých platformách. Součástí, která spravuje protokoly, je spousta běžných funkcí. Bude muset přijímat zprávy z jakékoli komponenty v systému. Tyto zprávy budou mít různé priority, které může základní komponenta spravovat. Zprávy by měly mít ve finální archivované podobě časová razítka. V pokročilejších scénářích můžete filtrovat zprávy podle zdrojové komponenty.

Existuje jeden aspekt funkce, který se často mění: kde se zapisují zprávy. V některých prostředích je možné je zapsat do konzoly chyb. V jiných je to soubor. Mezi další možnosti patří úložiště databáze, protokoly událostí operačního systému nebo jiné úložiště dokumentů.

Existují také kombinace výstupu, které se můžou použít v různých scénářích. Můžete chtít zapisovat zprávy do konzoly a do souboru.

Návrh založený na delegátech bude poskytovat velkou flexibilitu a usnadňuje podporu mechanismů úložiště, které mohou být přidány v budoucnu.

V rámci tohoto návrhu může být primární komponentou protokolu ne virtuální, dokonce i zapečetěná třída. Můžete připojit libovolnou sadu delegátů pro zápis zpráv na různá úložná média. Integrovaná podpora pro delegáty vícesměrového vysílání usnadňuje podporu scénářů, ve kterých musí být zprávy zapsány do více umístění (souboru a konzoly).

První implementace

Začínáme v malém: počáteční implementace přijme nové zprávy a zapíše je pomocí jakéhokoli připojeného delegáta. Můžete začít s jedním delegátem, který zapisuje zprávy do konzoly.

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

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

Výše uvedená statická třída je nejjednodušší věcí, která může fungovat. Potřebujeme napsat jednu implementaci pro metodu , která zapisuje zprávy do konzoly:

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

Nakonec je potřeba připojit delegáta tak, že ho připojíte k delegátu WriteMessage deklarovaného v protokolovači:

Logger.WriteMessage += LoggingMethods.LogToConsole;

Postupy

Náš vzorek je zatím poměrně jednoduchý, ale stále ukazuje některé důležité pokyny pro návrhy zahrnující delegáty.

Použití typů delegátů definovaných v základní rozhraní usnadňuje uživatelům práci s delegáty. Nemusíte definovat nové typy a vývojáři, kteří používají vaši knihovnu, se nemusí učit nové specializované typy delegátů.

Použitá rozhraní jsou co nejmenší a nej flexibilnější: Pokud chcete vytvořit nový výstupní protokolovací nástroj, musíte vytvořit jednu metodu. Tato metoda může být statická metoda nebo metoda instance. Může mít libovolný přístup.

Formát výstupu

Pojďme tuto první verzi trochu robustnější a pak začít vytvářet další mechanismy protokolování.

Teď do metody přidáme několik argumentů, aby vaše třída protokolu vytvářely LogMessage() strukturovanější zprávy:

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}";
        WriteMessage(outputMsg);
    }
}

Teď tento argument použijeme k filtrování zpráv, které se Severity odesílané do výstupu protokolu.

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}";
        WriteMessage(outputMsg);
    }
}

Postupy

Do infrastruktury protokolování jste přidali nové funkce. Vzhledem k tomu, že komponenta protokolovacího nástroje je velmi volně svázat s libovolným výstupním mechanismem, mohou být tyto nové funkce přidány bez jakéhokoli dopadu na jakýkoli kód implementující delegát protokolovacího nástroje.

Jak budete pokračovat v sestavování, uvidíte další příklady toho, jak tato volná párování umožňuje větší flexibilitu při aktualizaci částí webu bez jakýchkoli změn v jiných umístěních. Ve větší aplikaci mohou být výstupní třídy protokolovacího nástroje v jiném sestavení a není dokonce nutné je znovu sestavovat.

Sestavení druhého výstupního modulu

Komponenta Log se chyste dobře. Přidejme ještě jeden výstupní modul, který do souboru protokoluje zprávy. To bude o něco více zapojený výstupní modul. Bude to třída, která zapouzdřuje operace se soubory a zajišťuje, aby byl soubor po každém zápisu vždy uzavřen. Tím se zajistí, že se všechna data po vygenerování každé zprávy vyprázdní na disk.

Tady je protokolovací nástroj založený na souboru:

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

Po vytvoření této třídy můžete vytvořit její instanci a její metodu LogMessage připojit ke komponentě Logger:

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

Tyto dvě se vzájemně nevyluují. Ke konzole a souboru můžete připojit obě metody protokolu a generovat zprávy:

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

Později, dokonce i ve stejné aplikaci, můžete odebrat jednoho z delegátů bez jakýchkoli dalších problémů se systémem:

Logger.WriteMessage -= LoggingMethods.LogToConsole;

Postupy

Teď jste přidali druhou obslužnou rutinu výstupu pro subsystém protokolování. Ten potřebuje trochu více infrastruktury, aby správně podporoval systém souborů. Delegát je metoda instance. Jedná se také o soukromou metodu. Není potřeba větší přístupnost, protože infrastruktura delegátů může připojit delegáty.

Návrh založený na delegátovi umožňuje více výstupních metod bez jakéhokoli dodatečného kódu. Není nutné vytvářet žádnou další infrastrukturu pro podporu více výstupních metod. Jednoduše se stávají další metodou v seznamu volání.

Věnujte zvláštní pozornost kódu ve výstupní metodě protokolování souboru. Je kódován tak, aby se zajistilo, že nevy vyvolá žádné výjimky. I když to není vždy nezbytně nutné, je to často dobrý postup. Pokud která z metod delegátu vyvolá výjimku, zbývající delegáti, kteří jsou na volání, nebudou vyvoláni.

Nakonec je třeba poznamenat, že protokolovací nástroj souborů musí spravovat své prostředky otevřením a zavřením souboru v každé zprávě protokolu. Po dokončení můžete soubor ponechat otevřený a implementovat a zavřít IDisposable ho. Která z těchto metod má své výhody a nevýhody. Obě třídy vytváří trochu více párování mezi třídami.

Žádný kód ve třídě není potřeba aktualizovat, aby podporoval Logger některý z těchto scénářů.

Zpracování delegátů s hodnotou Null

Nakonec aktualizujeme metodu LogMessage tak, aby byla robustní pro ty případy, kdy není vybraný žádný výstupní mechanismus. Aktuální implementace vyvolá výjimku , pokud delegát nemá připojený NullReferenceException WriteMessage seznam vyvolání. Můžete preferovat návrh, který bezobslužně pokračuje, když nejsou připojené žádné metody. To je snadné pomocí podmíněného operátoru null v kombinaci s Delegate.Invoke() metodou :

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

Podmíněný operátor null ( ) je krátký, pokud je levý operand (v tomto případě) null, což znamená, že se ?. nepokusí WriteMessage protokolovat zprávu.

Metodu uvedenou v dokumentaci pro nebo Invoke() System.Delegate System.MulticastDelegate nenajdete. Kompilátor generuje typvě bezpečnou Invoke metodu pro libovolný typ delegátu deklarovaný. V tomto příkladu to Invoke znamená, že přebírá string jeden argument a má návratový typ void.

Shrnutí postupů

Viděli jste začátek komponenty protokolu, kterou je možné rozšířit o další zapisovače a další funkce. Při použití delegátů v návrhu jsou tyto různé komponenty volně svázat. To poskytuje několik výhod. Je snadné vytvořit nové výstupní mechanismy a připojit je k systému. Tyto další mechanismy potřebují pouze jednu metodu: metodu, která zapisuje zprávu protokolu. Jedná se o návrh, který je odolný při přidání nových funkcí. Kontrakt vyžadované pro všechny zapisovače je implementovat jednu metodu. Tato metoda může být statická metoda nebo metoda instance. Může to být veřejný, soukromý nebo jakýkoli jiný právní přístup.

Třída Logger může provádět libovolná vylepšení nebo změny, aniž by rušily změny. Stejně jako u jakékoli jiné třídy nemůžete veřejné rozhraní API upravit bez rizika, že by se změny rozbíjí. Vzhledem k tomu, že párování mezi protokolovačem a libovolnými výstupními moduly je pouze prostřednictvím delegátu, nejsou zapojeny žádné jiné typy (jako jsou rozhraní nebo základní třídy). Párování je co nejmenší.

Další