Megosztás a következőn keresztül:


A meghatalmazottak gyakori mintái

Előző

A meghatalmazottak olyan mechanizmust biztosítanak, amely lehetővé teszi az összetevők közötti minimális összekapcsolást magában foglaló szoftverterveket.

Az ilyen típusú kialakításra kiváló példa a LINQ. A LINQ lekérdezési kifejezési minta minden funkciójához delegáltakra támaszkodik. Tekintse meg ezt az egyszerű példát:

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

Ez a számsort csak a 10-nél kisebb értékekre szűri. A Where metódus egy delegáltat használ, amely meghatározza, hogy a sorozat mely elemei haladnak át a szűrőn. LINQ-lekérdezés létrehozásakor meg kell adnia a meghatalmazott implementációját erre a célra.

A Where metódus prototípusa:

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

Ez a példa a LINQ részét képező összes metódussal ismétlődik. Mindegyik az adott lekérdezést kezelő kód delegáltjaira támaszkodik. Ez az API-tervezési minta egy erőteljes eszköz, amelyet érdemes megtanulni és megérteni.

Ez az egyszerű példa azt szemlélteti, hogy a delegáltaknak nagyon kevés kapcsolatra van szükségük az összetevők között. Nem kell olyan osztályt létrehoznia, amely egy adott alaposztályból származik. Nem kell implementálnia egy adott felületet. Az egyetlen követelmény az, hogy egy, a feladat szempontjából alapvető fontosságú módszer implementálását biztosítsuk.

Hozzon létre saját összetevőket delegátusokkal

Erre a példára építve hozzunk létre egy olyan összetevőt, amely a meghatalmazottakra támaszkodik.

Határozzunk meg egy olyan összetevőt, amely egy nagy rendszerben használható naplóüzenetekhez. A kódtár összetevői számos különböző környezetben, több különböző platformon használhatók. A naplókat kezelő összetevő számos gyakori funkciót tartalmaz. A rendszer bármely összetevőjének üzeneteit el kell fogadnia. Ezek az üzenetek különböző prioritásokkal rendelkeznek, amelyeket az alapvető összetevő kezelni tud. Az üzeneteknek időbélyegzőkkel kell rendelkezniük a végleges archivált formájukban. Speciálisabb forgatókönyvek esetén szűrheti az üzeneteket a forrásösszetevő alapján.

A funkciónak van egy olyan aspektusa, amely gyakran változik: az üzenetek írásának helye. Egyes környezetekben előfordulhat, hogy a hibakonzolra írnak. Máshol egy fájl. Más lehetőségek közé tartozik az adatbázis-tárolás, az operációs rendszer eseménynaplói vagy más dokumentumtárolás.

A kimenetnek vannak olyan kombinációi is, amelyek különböző forgatókönyvekben használhatók. Érdemes lehet üzeneteket írni a konzolra és egy fájlba.

A meghatalmazottakon alapuló kialakítás nagy rugalmasságot biztosít, és megkönnyíti a jövőben hozzáadható tárolási mechanizmusok támogatását.

Ebben a kialakításban az elsődleges naplóösszetevő lehet nem virtuális, akár lezárt osztály is. Az üzenetek különböző tárolókba való írásához bármilyen meghatalmazottat csatlakoztathat. A csoportos küldési meghatalmazottak beépített támogatása megkönnyíti azokat a forgatókönyveket, amelyekben az üzeneteket több helyre (fájlba és konzolra) kell írni.

Első implementáció

Kezdjük kicsivel: a kezdeti implementáció elfogadja az új üzeneteket, és bármilyen csatolt meghatalmazott használatával megírja őket. Kezdjük azzal a meghatalmazottal, aki üzeneteket ír a konzolra.

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

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

A fenti statikus osztály a legegyszerűbb dolog, ami működik. Meg kell írnunk az üzeneteket a konzolra író metódus egyetlen implementációját:

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

Végül csatlakoztatnia kell a delegáltat a naplózóban deklarált WriteMessage delegálthoz.

Logger.WriteMessage += LoggingMethods.LogToConsole;

Gyakorlatok

Az eddigi minta meglehetősen egyszerű, de még mindig bemutatja a meghatalmazottakat érintő tervek néhány fontos irányelvét.

Az alapvető keretrendszerben definiált delegálási típusok használatával a felhasználók könnyebben dolgozhatnak a meghatalmazottakkal. Nem kell új típusokat definiálnia, és a kódtárat használó fejlesztőknek nem kell új, speciális delegálási típusokat megtanulniuk.

A használt felületek a lehető legkisebbek és rugalmasak: Új kimeneti naplózó létrehozásához létre kell hoznia egy metódust. Ez a módszer lehet statikus vagy példánymetódus. Lehet, hogy bármilyen hozzáféréssel rendelkezik.

Kimenet formázása

Tegyük ezt az első verziót egy kicsit robusztusabbá, majd hozzunk létre más naplózási mechanizmusokat.

Ezután adjunk hozzá néhány argumentumot a LogMessage() metódushoz, hogy a naplóosztály strukturáltabb üzeneteket hozzon létre:

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

Ezután használjuk ezt az Severity argumentumot a napló kimenetére küldött üzenetek szűréséhez.

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

Gyakorlatok

Új funkciókat adott hozzá a naplózási infrastruktúrához. Mivel a naplózó összetevő nagyon lazán kapcsolódik bármely kimeneti mechanizmushoz, ezek az új funkciók a naplózó delegáltját implementáló kód egyikére sem lesznek hatással.

Ahogy ezt tovább építi, további példákat láthat arra, hogy ez a laza összekapcsolás hogyan teszi lehetővé a webhely egyes részeinek nagyobb rugalmasságát anélkül, hogy más helyeket módosítanak. Valójában egy nagyobb alkalmazásban a naplózó kimeneti osztályai egy másik szerelvényben lehetnek, és nem is kell újraépíteni.

Második kimeneti motor létrehozása

A napló komponens jól halad. Adjunk hozzá még egy kimeneti motort, amely naplózza az üzeneteket egy fájlban. Ez egy kicsit összetettebb kimeneti rendszer lesz. Ez egy osztály lesz, amely beágyazza a fájlműveleteket, és biztosítja, hogy a fájl minden írás után mindig zárva legyen. Ez biztosítja, hogy az egyes üzenetek létrehozása után az összes adat ki legyen ürítve a lemezre.

Ez a fájlalapú naplózó:

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

Miután létrehozta ezt az osztályt, példányosíthatja azt, és a LogMessage metódust a Logger összetevőhöz csatolja:

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

Ez a kettő nem zárja ki egymást. A naplómetelyeket is csatolhatja, és üzeneteket hozhat létre a konzolon és egy fájlban:

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

Később, még ugyanabban az alkalmazásban is eltávolíthatja az egyik meghatalmazottat anélkül, hogy bármilyen más probléma merült fel a rendszerben:

Logger.WriteMessage -= LoggingMethods.LogToConsole;

Gyakorlatok

Most hozzáadott egy második kimeneti kezelőt a naplózási alrendszerhez. Ehhez egy kicsit több infrastruktúrára van szükség a fájlrendszer megfelelő támogatásához. A delegált egy példánymetódus. Ez egy privát módszer is. Nincs szükség nagyobb akadálymentességre, mert a meghatalmazotti infrastruktúra képes csatlakoztatni a meghatalmazottakat.

Másodszor, a delegáltalapú kialakítás több kimeneti metódust is lehetővé tesz további kód nélkül. Nem kell további infrastruktúrát létrehoznia több kimeneti módszer támogatásához. Egyszerűen egy másik metódussá válnak a meghívási listán.

Különös figyelmet kell fordítani a fájlnaplózás kimeneti metódusában található kódra. Úgy van megírva a kód, hogy ne dobjon kivételeket. Bár ez nem mindig feltétlenül szükséges, ez gyakran jó gyakorlat. Ha a delegálási metódusok bármelyike kivételt jelez, a meghívásban szereplő többi meghatalmazott nem lesz meghívva.

Utolsó megjegyzésként a fájlnaplózónak az egyes naplóüzenetek fájljának megnyitásával és bezárásával kell kezelnie az erőforrásait. Úgy is dönthet, hogy a fájl nyitva marad, és amikor elkészült, IDisposable segítségével zárja be a fájlt. Bármelyik módszernek megvannak az előnyei és hátrányai. Mindkettő egy kicsit jobban összekapcsolja az osztályokat.

Az osztály egyik Logger kódját sem kell frissíteni mindkét forgatókönyv támogatásához.

Null delegátumok kezelése

Végül frissítse a LogMessage metódust, hogy az olyan esetekben is hatékony legyen, amikor nincs kimeneti mechanizmus kiválasztva. Az aktuális implementáció NullReferenceException kivételt dob, amikor a WriteMessage delegált nem rendelkezik csatolt meghívási listával. Érdemes lehet olyan tervezést használni, amely csendben folytatódik, amikor nem lettek metódusok csatolva. A null feltételes operátor használata egyszerű, a Delegate.Invoke() metódussal kombinálva:

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

A null-lekérdezési operátor (?.) leáll, ha a bal oldali operandus (WriteMessage ebben az esetben) null értékű, ami azt jelenti, hogy nem történik kísérlet az üzenet naplózására.

A Invoke() metódust sem System.Delegate, sem System.MulticastDelegate dokumentációjában nem találja. A fordító minden deklarált delegált típushoz létrehoz egy típusbiztos Invoke metódust. Ebben a példában ez azt jelenti Invoke , hogy egyetlen string argumentumot vesz fel, és érvénytelen visszatérési típussal rendelkezik.

A gyakorlatok összefoglalása

Láttad egy naplóösszetevő kezdeteit, amit más írókkal és más funkciókkal lehet bővíteni. Ha delegátokat használ a tervezésben, ezek a különböző összetevők lazán kapcsolódnak egymáshoz. Ez számos előnnyel jár. Könnyen létrehozhat új kimeneti mechanizmusokat, és csatolhatja őket a rendszerhez. Ezeknek a mechanizmusoknak csak egy metódusra van szükségük: a naplóüzenetet megíró metódusra. Ez egy olyan kialakítás, amely rugalmas az új funkciók hozzáadásakor. Az író számára kötelező szerződés az, hogy megvalósítsanak egy módszert. Ez a módszer lehet statikus vagy példánymetódus. Lehet nyilvános, privát vagy bármilyen más jogi hozzáférés.

A Logger osztály bármilyen számú fejlesztést vagy módosítást hajthat végre anélkül, hogy kompatibilitástörő módosításokat vezet be. Mint minden osztály, a nyilvános API-t sem módosíthatja a módosítások feltörésének veszélye nélkül. Mivel azonban a naplózó és a kimeneti motorok közötti összekapcsolás csak a delegálton keresztül történik, más típusok (például interfészek vagy alaposztályok) nem vesznek részt. A csatlakozó a lehető legkisebb.

Következő