Vanliga mönster för ombud
Ombud tillhandahåller en mekanism som möjliggör programvarudesign med minimal koppling mellan komponenter.
Ett utmärkt exempel på den här typen av design är LINQ. LINQ-frågeuttrycksmönstret förlitar sig på ombud för alla dess funktioner. Tänk på det här enkla exemplet:
var smallNumbers = numbers.Where(n => n < 10);
Detta filtrerar sekvensen med tal till endast de som är mindre än värdet 10.
Metoden Where
använder ett ombud som avgör vilka element i en sekvens som skickar filtret. När du skapar en LINQ-fråga anger du implementeringen av ombudet för det här specifika syftet.
Prototypen för where-metoden är:
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);
Det här exemplet upprepas med alla metoder som ingår i LINQ. De förlitar sig alla på ombud för den kod som hanterar den specifika frågan. Det här API-designmönstret är kraftfullt för att lära sig och förstå.
Det här enkla exemplet illustrerar hur ombud kräver mycket lite koppling mellan komponenter. Du behöver inte skapa en klass som härleds från en viss basklass. Du behöver inte implementera ett specifikt gränssnitt. Det enda kravet är att tillhandahålla implementeringen av en metod som är grundläggande för den aktuella uppgiften.
Skapa egna komponenter med ombud
Nu ska vi bygga vidare på det exemplet genom att skapa en komponent med hjälp av en design som förlitar sig på ombud.
Nu ska vi definiera en komponent som kan användas för loggmeddelanden i ett stort system. Bibliotekskomponenterna kan användas i många olika miljöer på flera olika plattformar. Det finns många vanliga funktioner i komponenten som hanterar loggarna. Den måste acceptera meddelanden från valfri komponent i systemet. Dessa meddelanden har olika prioriteter, som kärnkomponenten kan hantera. Meddelandena ska ha tidsstämplar i sitt slutliga arkiverade formulär. För mer avancerade scenarier kan du filtrera meddelanden efter källkomponenten.
Det finns en aspekt av funktionen som ändras ofta: var meddelanden skrivs. I vissa miljöer kan de skrivas till felkonsolen. I andra, en fil. Andra möjligheter är databaslagring, os-händelseloggar eller annan dokumentlagring.
Det finns också kombinationer av utdata som kan användas i olika scenarier. Du kanske vill skriva meddelanden till konsolen och till en fil.
En design baserad på ombud ger stor flexibilitet och gör det enkelt att stödja lagringsmekanismer som kan läggas till i framtiden.
Under den här designen kan den primära loggkomponenten vara en icke-virtuell, till och med förseglad klass. Du kan ansluta valfri uppsättning ombud för att skriva meddelandena till olika lagringsmedier. Det inbyggda stödet för multicast-ombud gör det enkelt att stödja scenarier där meddelanden måste skrivas till flera platser (en fil och en konsol).
En första implementering
Vi börjar i liten skala: den första implementeringen accepterar nya meddelanden och skriver dem med hjälp av eventuella anslutna ombud. Du kan börja med ett ombud som skriver meddelanden till konsolen.
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(string msg)
{
if (WriteMessage is not null)
WriteMessage(msg);
}
}
Den statiska klassen ovan är det enklaste som kan fungera. Vi måste skriva den enskilda implementeringen för metoden som skriver meddelanden till konsolen:
public static class LoggingMethods
{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}
Slutligen måste du ansluta ombudet genom att koppla det till WriteMessage-ombudet som deklarerats i loggern:
Logger.WriteMessage += LoggingMethods.LogToConsole;
Bästa metoder
Vårt exempel hittills är ganska enkelt, men det visar fortfarande några av de viktiga riktlinjerna för design som involverar delegater.
Genom att använda de ombudstyper som definierats i kärnramverket blir det enklare för användarna att arbeta med ombuden. Du behöver inte definiera nya typer och utvecklare som använder ditt bibliotek behöver inte lära sig nya, specialiserade ombudstyper.
De gränssnitt som används är så minimala och flexibla som möjligt: Om du vill skapa en ny utdataloggare måste du skapa en metod. Den metoden kan vara en statisk metod eller en instansmetod. Den kan ha åtkomst.
Formatera utdata
Nu ska vi göra den här första versionen lite mer robust och sedan börja skapa andra loggningsmekanismer.
Nu ska vi lägga till några argument i metoden så att loggklassen LogMessage()
skapar fler strukturerade meddelanden:
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);
}
}
Nu ska vi använda argumentet Severity
för att filtrera de meddelanden som skickas till loggens utdata.
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);
}
}
Bästa metoder
Du har lagt till nya funktioner i loggningsinfrastrukturen. Eftersom loggningskomponenten är mycket löst kopplad till alla utdatamekanismer kan dessa nya funktioner läggas till utan någon inverkan på någon av koden som implementerar loggningsdelegaten.
När du fortsätter att skapa detta ser du fler exempel på hur den här lösa kopplingen ger större flexibilitet när det gäller att uppdatera delar av webbplatsen utan några ändringar på andra platser. I ett större program kan loggningsutdataklasserna vara i en annan sammansättning och behöver inte ens återskapas.
Skapa en andra utdatamotor
Loggkomponenten kommer bra överens. Nu ska vi lägga till ytterligare en utdatamotor som loggar meddelanden till en fil. Detta kommer att vara en något mer involverad utdatamotor. Det är en klass som kapslar in filåtgärderna och ser till att filen alltid stängs efter varje skrivning. Det säkerställer att alla data rensas till disken när varje meddelande har genererats.
Här är den filbaserade loggern:
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.
}
}
}
När du har skapat den här klassen kan du instansiera den och den bifogar sin LogMessage-metod till Logger-komponenten:
var file = new FileLogger("log.txt");
Dessa två utesluter inte varandra. Du kan bifoga båda loggmetoderna och generera meddelanden till konsolen och en fil:
var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier
Senare, även i samma program, kan du ta bort en av ombuden utan några andra problem i systemet:
Logger.WriteMessage -= LoggingMethods.LogToConsole;
Bästa metoder
Nu har du lagt till en andra utdatahanterare för loggningsundersystemet. Den här behöver lite mer infrastruktur för att stödja filsystemet korrekt. Ombudet är en instansmetod. Det är också en privat metod. Det finns inget behov av större tillgänglighet eftersom ombudsinfrastrukturen kan ansluta ombuden.
För det andra möjliggör den delegerade designen flera utdatametoder utan extra kod. Du behöver inte skapa någon ytterligare infrastruktur för att stödja flera utdatametoder. De blir helt enkelt en annan metod i listan över anrop.
Var särskilt uppmärksam på koden i utdatametoden för filloggning. Den är kodad för att säkerställa att den inte utlöser några undantag. Även om detta inte alltid är absolut nödvändigt är det ofta en bra idé. Om någon av de delegerade metoderna utlöser ett undantag anropas inte de återstående ombuden som finns på anropet.
Som en sista kommentar måste filloggaren hantera sina resurser genom att öppna och stänga filen i varje loggmeddelande. Du kan välja att hålla filen öppen och implementera IDisposable
för att stänga filen när du är klar.
Båda metoderna har sina fördelar och nackdelar. Båda skapar lite mer koppling mellan klasserna.
Ingen av koden i Logger
klassen skulle behöva uppdateras för att stödja något av scenariona.
Hantera null-ombud
Slutligen ska vi uppdatera Metoden LogMessage så att den är robust för de fall då ingen utdatamekanism har valts. Den aktuella implementeringen genererar en NullReferenceException
när ombudet WriteMessage
inte har någon bifogad anropslista.
Du kanske föredrar en design som tyst fortsätter när inga metoder har kopplats. Det här är enkelt att använda null-villkorsoperatorn i kombination med Delegate.Invoke()
metoden:
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
Den villkorliga operatorn null (?.
) kortsluter när den vänstra operanden (WriteMessage
i det här fallet) är null, vilket innebär att inget försök görs att logga ett meddelande.
Du hittar inte metoden Invoke()
som anges i dokumentationen för System.Delegate
eller System.MulticastDelegate
. Kompilatorn genererar en typsäker Invoke
metod för alla deklarerade ombudstyper. I det här exemplet innebär Invoke
det att tar ett enda string
argument och har en typ av ogiltig retur.
Sammanfattning av metoder
Du har sett början på en loggkomponent som kan utökas med andra författare och andra funktioner. Genom att använda ombud i designen är dessa olika komponenter löst kopplade. Detta ger flera fördelar. Det är enkelt att skapa nya utdatamekanismer och koppla dem till systemet. Dessa andra mekanismer behöver bara en metod: den metod som skriver loggmeddelandet. Det är en design som är motståndskraftig när nya funktioner läggs till. Kontraktet som krävs för alla skrivare är att implementera en metod. Den metoden kan vara en statisk metod eller instansmetod. Det kan vara offentlig, privat eller någon annan juridisk åtkomst.
Klassen Logger kan göra valfritt antal förbättringar eller ändringar utan att införa icke-bakåtkompatibla ändringar. Precis som alla klasser kan du inte ändra det offentliga API:et utan risk för icke-bakåtkompatibla ändringar. Men eftersom kopplingen mellan loggern och alla utdatamotorer bara sker via ombudet, är inga andra typer (till exempel gränssnitt eller basklasser) inblandade. Kopplingen är så liten som möjligt.