Sdílet prostřednictvím


Běžné vzory pro delegáty

Předchozí

Delegáti poskytují mechanismus, který umožňuje návrh softwaru zahrnující minimální párování mezi součástmi.

Jedním z vynikajících příkladů pro tento druh návrhu je LINQ. Vzor výrazu dotazu LINQ spoléhá na delegáty pro všechny jeho funkce. Podívejte se na tento jednoduchý příklad:

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

Tím se vyfiltruje posloupnost čísel pouze na čísla menší než hodnota 10. Metoda Where používá delegáta, který určuje, které prvky sekvence předávají 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ý, abyste se naučili a porozuměli.

Tento jednoduchý příklad ukazuje, jak delegáti vyžadují velmi málo párování mezi komponentami. Nemusíte 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 poskytnout implementaci jedné metody, která je zásadní pro daný úkol.

Sestavení vlastních komponent pomocí delegátů

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

Pojďme definovat komponentu, která se dá použít pro zprávy protokolu ve velkém systému. Komponenty knihovny je možné použít v mnoha různých prostředích na několika různých platformách. Součástí je spousta běžných funkcí, které spravují protokoly. Bude muset přijímat zprávy z libovolné 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 časová razítka v konečné archivované podobě. 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 zprávy zapisují. V některých prostředích mohou být zapsány do konzoly chyb. V jiných soubor. Mezi další možnosti patří úložiště databází, protokoly událostí operačního systému nebo jiné úložiště dokumentů.

Existují také kombinace výstupu, které se dají použít v různých scénářích. Možná budete chtít psát zprávy do konzoly a do souboru.

Návrh založený na delegátech poskytuje 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í komponenta protokolu ne virtuální, dokonce zapečetěnou třídou. K zápisu zpráv do různých úložných médií můžete připojit libovolnou sadu delegátů. Integrovaná podpora pro delegáty vícesměrového vysílání usnadňuje podporu scénářů, kdy se zprávy musí zapisovat do více umístění (soubor a konzola).

První implementace

Začněme malou: počáteční implementace přijme nové zprávy a zapíše je pomocí 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)
    {
        if (WriteMessage is not null)
            WriteMessage(msg);
    }
}

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

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

Nakonec musíte připojit delegáta tak, že ho připojíte k delegátu WriteMessage deklarovanému v protokolovacím nástroji:

Logger.WriteMessage += LoggingMethods.LogToConsole;

Postupy

Naše ukázka je zatím poměrně jednoduchá, ale přesto ukazuje některé důležité pokyny pro návrhy zahrnující delegáty.

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

Použitá rozhraní jsou co nejmenší a co nejflexibilnější: Chcete-li 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 jakýkoli přístup.

Formát výstupu

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

V dalším kroku přidáme do LogMessage() metody několik argumentů, aby třída protokolu vytvářela 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}";
        if (WriteMessage is not null)
            WriteMessage(outputMsg);
    }
}

V dalším kroku použijeme tento Severity argument k filtrování zpráv odesílaných 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}";
        if (WriteMessage is not null)
            WriteMessage(outputMsg);
    }
}

Postupy

Do infrastruktury protokolování jste přidali nové funkce. Vzhledem k tomu, že součást loggeru je velmi volně svázána s jakýmkoli výstupním mechanismem, lze tyto nové funkce přidat bez dopadu na žádný kód implementovaný delegát protokolovacím nástrojem.

Při vytváření této dokumentace uvidíte další příklady toho, jak tato volná spojka umožňuje větší flexibilitu při aktualizaci částí lokality bez jakýchkoli změn v jiných umístěních. Ve větší aplikaci můžou být výstupní třídy loggeru v jiném sestavení a nemusí být ani znovu sestaveny.

Vytvoření druhého výstupního modulu

Součástí protokolu je dobře. Pojďme přidat další výstupní modul, který protokoluje zprávy do souboru. Bude to o něco více zapojený výstupní modul. Bude to třída, která zapouzdřuje operace se soubory a zajišťuje, že soubor bude vždy uzavřen po každém zápisu. Tím zajistíte, že se všechna data vyprázdní na disk po vygenerování každé zprávy.

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

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

Jakmile vytvoříte tuto třídu, můžete ji vytvořit instanci a připojí její metodu LogMessage ke komponentě Logger:

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

Tyto dvě se vzájemně nevylučují. K konzole a souboru můžete připojit 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, i ve stejné aplikaci, můžete odebrat jednoho z delegátů bez jakýchkoli jiných problémů v systému:

Logger.WriteMessage -= LoggingMethods.LogToConsole;

Postupy

Teď jste přidali druhou obslužnou rutinu výstupu pro subsystém protokolování. Tato infrastruktura potřebuje k správné podpoře systému souborů trochu více infrastruktury. Delegát je metoda instance. Je to také soukromá metoda. Není potřeba mít větší přístupnost, protože delegovat infrastrukturu může připojit delegáty.

Za druhé návrh založený na delegátech umožňuje více výstupních metod bez jakéhokoli dalšího kódu. Pro podporu více výstupních metod nemusíte vytvářet žádnou další infrastrukturu. Jednoduše se stanou další metodou v seznamu vyvolání.

Věnujte zvláštní pozornost kódu v metodě výstupu protokolování souboru. Kóduje se, aby se zajistilo, že nevyvolá žádné výjimky. I když to není vždy nezbytně nutné, je to často dobrý postup. Pokud některý z metod delegáta vyvolá výjimku, zbývající delegáti, které jsou na vyvolání, nebudou vyvolány.

Jako poslední poznámka, protokolovací nástroj musí spravovat své prostředky otevřením a zavřením souboru v každé zprávě protokolu. Soubor můžete nechat otevřený a implementovat IDisposable , abyste soubor po dokončení zavřeli. Obě metody mají své výhody a nevýhody. Oba vytvářejí trochu více párování mezi třídami.

Žádný kód ve Logger třídě by se nemusel aktualizovat, aby podporoval některý ze scénářů.

Zpracování delegátů s hodnotou Null

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

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

Podmíněný operátor s hodnotou null (?.) zkratuje, když levý operand (WriteMessage v tomto případě) má hodnotu null, což znamená, že se neprovedou žádné pokusy o protokolování zprávy.

Metodu Invoke() uvedenou v dokumentaci nebo System.MulticastDelegatev této dokumentaci System.Delegate nenajdete. Kompilátor vygeneruje metodu bezpečného Invoke typu pro libovolný deklarovaný typ delegáta. V tomto příkladu to znamená Invoke , že přebírá jeden string argument a má návratový typ void.

Souhrn postupů

Viděli jste začátek komponenty protokolu, která by mohla být rozšířena o další zapisovače a další funkce. Pomocí delegátů v návrhu jsou tyto různé komponenty volně svázány. 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. Je to návrh, který je odolný při přidání nových funkcí. Kontrakt vyžadovaný pro jakýkoli zapisovač 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ý počet vylepšení nebo změn bez zavedení zásadních změn. Stejně jako u jakékoli třídy nemůžete změnit veřejné rozhraní API bez rizika zásadních změn. Vzhledem k tomu, že spojení mezi protokolovacím a výstupním motorem je pouze prostřednictvím delegáta, nejsou zapojeny žádné jiné typy (například rozhraní nebo základní třídy). Spojka je co nejmenší.

Další