Delen via


Algemene patronen voor gemachtigden

Vorige

Gedelegeerden bieden een mechanisme waarmee softwareontwerpen met minimale koppeling tussen onderdelen mogelijk zijn.

Een uitstekend voorbeeld voor dit type ontwerp is LINQ. Het LINQ-queryexpressiepatroon is afhankelijk van gemachtigden voor alle functies. Bekijk dit eenvoudige voorbeeld:

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

Hiermee wordt de reeks getallen gefilterd op alleen getallen die kleiner zijn dan de waarde 10. De Where methode maakt gebruik van een gemachtigde die bepaalt welke elementen van een reeks het filter doorgeven. Wanneer u een LINQ-query maakt, geeft u de implementatie van de gemachtigde op voor dit specifieke doel.

Het prototype voor de where-methode is:

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

Dit voorbeeld wordt herhaald met alle methoden die deel uitmaken van LINQ. Ze zijn allemaal afhankelijk van gemachtigden voor de code die de specifieke query beheert. Dit API-ontwerppatroon is een krachtig patroon om te leren en te begrijpen.

In dit eenvoudige voorbeeld ziet u hoe gedelegeerden weinig koppeling tussen onderdelen vereisen. U hoeft geen klasse te maken die is afgeleid van een bepaalde basisklasse. U hoeft geen specifieke interface te implementeren. Het enige vereiste is om de implementatie van één methode te bieden die fundamenteel is voor de taak die bij de hand is.

Uw eigen onderdelen bouwen met gemachtigden

Laten we verder gaan met dat voorbeeld door een onderdeel te maken met behulp van een ontwerp dat afhankelijk is van gedelegeerden.

Laten we een onderdeel definiëren dat kan worden gebruikt voor logboekberichten in een groot systeem. De bibliotheekonderdelen kunnen worden gebruikt in veel verschillende omgevingen, op meerdere verschillende platforms. Er zijn veel algemene functies in het onderdeel waarmee de logboeken worden beheerd. Het moet berichten van elk onderdeel in het systeem accepteren. Deze berichten hebben verschillende prioriteiten, die het kernonderdeel kan beheren. De berichten moeten tijdstempels hebben in hun uiteindelijke gearchiveerde formulier. Voor meer geavanceerde scenario's kunt u berichten filteren op het brononderdeel.

Er is één aspect van de functie die vaak verandert: waar berichten worden geschreven. In sommige omgevingen kunnen ze naar de foutconsole worden geschreven. In anderen, een bestand. Andere mogelijkheden zijn databaseopslag, gebeurtenislogboeken van het besturingssysteem of andere documentopslag.

Er zijn ook combinaties van uitvoer die in verschillende scenario's kunnen worden gebruikt. Mogelijk wilt u berichten naar de console en naar een bestand schrijven.

Een ontwerp op basis van gedelegeerden biedt veel flexibiliteit en maakt het eenvoudig om opslagmechanismen te ondersteunen die in de toekomst kunnen worden toegevoegd.

Onder dit ontwerp kan het primaire logboekonderdeel een niet-virtuele, zelfs verzegelde klasse zijn. U kunt elke set gemachtigden instellen om de berichten naar verschillende opslagmedia te schrijven. De ingebouwde ondersteuning voor multicast-gemachtigden maakt het eenvoudig om scenario's te ondersteunen waarbij berichten naar meerdere locaties moeten worden geschreven (een bestand en een console).

Een eerste implementatie

Laten we beginnen: de eerste implementatie accepteert nieuwe berichten en schrijft deze met behulp van een bijgevoegde gemachtigde. U kunt beginnen met één gemachtigde die berichten naar de console schrijft.

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

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

De bovenstaande statische klasse is het eenvoudigste wat kan werken. We moeten de enkele implementatie schrijven voor de methode waarmee berichten naar de console worden geschreven:

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

Ten slotte moet u de gemachtigde koppelen door deze te koppelen aan de WriteMessage-gemachtigde die is gedeclareerd in de logger:

Logger.WriteMessage += LoggingMethods.LogToConsole;

Procedures

Ons voorbeeld tot nu toe is redelijk eenvoudig, maar het toont nog steeds enkele belangrijke richtlijnen voor ontwerpen met gedelegeerden.

Door de gedelegeerdentypen te gebruiken die zijn gedefinieerd in het kernframework, kunnen gebruikers gemakkelijker met de gemachtigden werken. U hoeft geen nieuwe typen te definiëren en ontwikkelaars die uw bibliotheek gebruiken, hoeven geen nieuwe, gespecialiseerde gedelegeerdentypen te leren.

De gebruikte interfaces zijn zo minimaal en zo flexibel mogelijk: als u een nieuwe uitvoerlogger wilt maken, moet u één methode maken. Deze methode kan een statische methode of een instantiemethode zijn. Het heeft mogelijk toegang.

Uitvoer opmaken

Laten we deze eerste versie een beetje robuuster maken en vervolgens andere mechanismen voor logboekregistratie maken.

Vervolgens gaan we een paar argumenten toevoegen aan de LogMessage() methode, zodat uw logboekklasse meer gestructureerde berichten maakt:

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

Laten we vervolgens dat argument gebruiken Severity om de berichten te filteren die naar de uitvoer van het logboek worden verzonden.

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

Procedures

U hebt nieuwe functies toegevoegd aan de logboekinfrastructuur. Omdat het loggeronderdeel zeer losjes is gekoppeld aan elk uitvoermechanisme, kunnen deze nieuwe functies worden toegevoegd zonder dat dit van invloed is op de code die de gemachtigde van de logger implementeert.

Terwijl u dit blijft bouwen, ziet u meer voorbeelden van hoe deze losse koppeling meer flexibiliteit biedt bij het bijwerken van delen van de site zonder dat er wijzigingen in andere locaties zijn. In een grotere toepassing bevinden de loggeruitvoerklassen zich mogelijk in een andere assembly en hoeven ze niet eens opnieuw te worden opgebouwd.

Een tweede uitvoerengine bouwen

Het logboekonderdeel komt goed langs. Laten we nog een uitvoerengine toevoegen waarmee berichten worden in een bestand opgeslagen. Dit is een iets meer betrokken uitvoerengine. Het is een klasse die de bestandsbewerkingen inkapselt en zorgt ervoor dat het bestand na elke schrijfbewerking altijd wordt gesloten. Dit zorgt ervoor dat alle gegevens naar de schijf worden leeggemaakt nadat elk bericht is gegenereerd.

Dit is die logboekregistratie op basis van bestanden:

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

Nadat u deze klasse hebt gemaakt, kunt u deze instantiëren en de LogMessage-methode koppelen aan het loggeronderdeel:

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

Deze twee sluiten elkaar niet uit. U kunt zowel logboekmethoden koppelen als berichten genereren aan de console en een bestand:

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

Later kunt u, zelfs in dezelfde toepassing, een van de gemachtigden verwijderen zonder andere problemen met het systeem:

Logger.WriteMessage -= LoggingMethods.LogToConsole;

Procedures

U hebt nu een tweede uitvoerhandler toegevoegd voor het subsysteem voor logboekregistratie. Deze heeft een beetje meer infrastructuur nodig om het bestandssysteem correct te ondersteunen. De gemachtigde is een instantiemethode. Het is ook een privémethode. Er is geen grotere toegankelijkheid nodig omdat de gedelegeerde-infrastructuur verbinding kan maken met de gedelegeerden.

Ten tweede maakt het ontwerp op basis van gedelegeerden meerdere uitvoermethoden mogelijk zonder extra code. U hoeft geen extra infrastructuur te bouwen om meerdere uitvoermethoden te ondersteunen. Ze worden gewoon een andere methode in de aanroeplijst.

Let vooral op de code in de uitvoermethode voor bestandslogboekregistratie. Het is gecodeerd om ervoor te zorgen dat er geen uitzonderingen worden gegooid. Hoewel dit niet altijd strikt noodzakelijk is, is het vaak een goede gewoonte. Als een van de gemachtigdenmethoden een uitzondering genereert, worden de resterende gemachtigden die zich in de aanroep bevinden, niet aangeroepen.

Als laatste opmerking moet de bestandslogger de resources beheren door het bestand te openen en te sluiten op elk logboekbericht. U kunt ervoor kiezen om het bestand geopend te houden en te implementeren IDisposable om het bestand te sluiten wanneer u klaar bent. Beide methoden hebben de voor- en nadelen. Beide maken een beetje meer koppeling tussen de klassen.

Geen van de code in de Logger klasse moet worden bijgewerkt om beide scenario's te ondersteunen.

Null-gemachtigden verwerken

Ten slotte gaan we de LogMessage-methode bijwerken, zodat deze robuust is voor die gevallen wanneer er geen uitvoermechanisme is geselecteerd. De huidige implementatie genereert een NullReferenceException moment waarop de WriteMessage gemachtigde geen aanroeplijst heeft gekoppeld. U kunt de voorkeur geven aan een ontwerp dat op de achtergrond wordt voortgezet wanneer er geen methoden zijn gekoppeld. Dit is eenvoudig met behulp van de voorwaardelijke operator null, gecombineerd met de Delegate.Invoke() methode:

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

De null-voorwaardelijke operator (?.) korte circuits wanneer de linkeroperand (WriteMessage in dit geval) null is, wat betekent dat er geen poging wordt gedaan om een bericht te registreren.

U vindt de Invoke() methode niet die wordt vermeld in de documentatie voor System.Delegate of System.MulticastDelegate. De compiler genereert een typeveilige Invoke methode voor elk gemachtigde type dat is gedeclareerd. In dit voorbeeld betekent dit dat Invoke één string argument wordt gebruikt en een ongeldig retourtype heeft.

Samenvatting van procedures

U hebt het begin gezien van een logboekonderdeel dat kan worden uitgebreid met andere schrijvers en andere functies. Door gedelegeerden in het ontwerp te gebruiken, zijn deze verschillende onderdelen losjes gekoppeld. Dit biedt verschillende voordelen. Het is eenvoudig om nieuwe uitvoermechanismen te maken en deze aan het systeem te koppelen. Deze andere mechanismen hebben slechts één methode nodig: de methode waarmee het logboekbericht wordt geschreven. Het is een ontwerp dat flexibel is wanneer nieuwe functies worden toegevoegd. Het contract dat nodig is voor elke schrijver is om één methode te implementeren. Deze methode kan een statische methode of instantiemethode zijn. Dit kan openbaar, privé of andere juridische toegang zijn.

De loggerklasse kan een willekeurig aantal verbeteringen of wijzigingen aanbrengen zonder wijzigingen die fouten veroorzaken. Net als elke klasse kunt u de openbare API niet wijzigen zonder het risico dat wijzigingen fouten veroorzaken. Maar omdat de koppeling tussen de logger en eventuele uitvoerengines alleen via de gemachtigde wordt uitgevoerd, zijn er geen andere typen (zoals interfaces of basisklassen) betrokken. De koppeling is zo klein mogelijk.

Volgende