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 silný a stojí za to se ho naučit a pochopit.

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 případech, 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 nemusí být primární komponenta protokolu virtuální, může jít dokonce o uzavřenou třídu. 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;

Praktiky

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 udělat tuto první verzi trochu více robustní, a poté začneme vytvářet další způsoby zaznamenává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);
    }
}

Praktiky

Do infrastruktury protokolování jste přidali nové funkce. Vzhledem k tomu, že komponent loggeru je velmi volně svázán s jakýmkoli výstupním mechanismem, lze tyto nové funkce přidávat bez dopadu na žádný kód implementující delegát loggeru.

Jak budete nadále budovat tento web, uvidíte více příkladů toho, jak volné propojení umožňuje větší flexibilitu při aktualizaci částí webu bez změn v jiných částech. 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

Komponenta logu se dobře vyvíjí. Pojďme přidat další výstupní modul, který protokoluje zprávy do souboru. Bude to poněkud složitější výstupní systém. 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 instancovat a připojí svou 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;

Praktiky

Nyní jste přidali druhou výstupní obslužnou rutinu pro subsystém protokolování. Tento prvek potřebuje k správné podpoře systému souborů trochu více infrastruktury. Delegát je instanční metoda. Je to také soukromá metoda. Není potřeba lepší dostupnost, protože infrastruktura pro delegáty 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.

Na závěr musí protokolovací nástroj spravovat své prostředky tím, že při každé zprávě do logu otevře a zavře soubor. 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 způsobují trochu více propojení mezi třídami.

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

Zpracování nulových delegátů

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 tehdy, když delegát WriteMessage nemá připojený seznam vyvolání. Můžete preferovat návrh, který tiše pokračuje, když nejsou 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() nenajdete uvedenou v dokumentaci pro System.Delegate ani System.MulticastDelegate. 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 požadovaný od jakéhokoli autora je implementovat alespoň 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 motorem a výstupním motorem je pouze prostřednictvím delegáta, nejsou zahrnuty žádné jiné typy (například rozhraní nebo základní třídy). Spojka je co nejmenší.

Další