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


Mintamegfeleltetés használata az osztály viselkedésének kialakításához a jobb kód érdekében

A C# mintamegfeleltetési funkciói szintaxist biztosítanak az algoritmusok kifejezéséhez. Ezekkel a technikákkal implementálhatja a viselkedést az osztályokban. Az objektumorientált osztálytervezést adatorientált implementációval kombinálva tömör kódot biztosíthat valós objektumok modellezése közben.

Az oktatóanyag segítségével megtanulhatja a következőket:

  • Az objektumorientált osztályokat adatmintákkal fejezheti ki.
  • Ezeket a mintákat a C#mintaegyező funkcióival valósíthatja meg.
  • A implementáció ellenőrzéséhez használja a fordítódiagnosztikát.

Előfeltételek

A .NET futtatásához be kell állítania a gépet. Töltse le a Visual Studio 2022-t vagy a .NET SDK-t.

Csatornazár szimulációjának létrehozása

Ebben az oktatóanyagban egy C#-osztályt fog létrehozni, amely csatornazárolást szimulál. Röviden, a csatornazár egy olyan eszköz, amely emeli és csökkenti a hajókat, miközben két vízszakasz között haladnak különböző szinteken. A zár két kapuval és valamilyen mechanizmussal rendelkezik a vízszint módosításához.

Normál működése során a hajó belép az egyik kapuba, míg a zsilip vízszintje megegyezik a hajó által beléptetett vízszinttel. Miután a zár, a víz szintje változik, hogy megfeleljen a vízszint, ahol a hajó elhagyja a zárat. Amint a vízszint megegyezik ezen az oldalon, megnyílik a kapu a kilépési oldalon. Széf mértékek biztosítják, hogy egy operátor ne hozzon létre veszélyes helyzetet a csatornában. A vízszint csak akkor módosítható, ha mindkét kapu zárva van. Legfeljebb egy kapu lehet nyitva. A kapu megnyitásához a zár vízszintjének meg kell egyeznie a nyitott kapun kívüli vízszinttel.

Ennek a viselkedésnek a modellezéséhez létrehozhat egy C#-osztályt. Egy CanalLock osztály támogatná a parancsokat bármelyik kapu megnyitásához vagy bezárásához. Más parancsokkal is rendelkezne a víz felemeléséhez vagy csökkentéséhez. Az osztálynak támogatnia kell a tulajdonságokat a kapuk és a vízszint aktuális állapotának olvasásához. A módszerek implementálják a biztonsági intézkedéseket.

Osztály definiálása

Egy konzolalkalmazást fog létrehozni az osztály teszteléséhez CanalLock . Hozzon létre egy új konzolprojektet a .NET 5-höz a Visual Studio vagy a .NET CLI használatával. Ezután adjon hozzá egy új osztályt, és nevezze el.CanalLock Ezután tervezzen meg egy nyilvános API-t, de hagyja a metódusokat nem implementálva:

public enum WaterLevel
{
    Low,
    High
}
public class CanalLock
{
    // Query canal lock state:
    public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
    public bool HighWaterGateOpen { get; private set; } = false;
    public bool LowWaterGateOpen { get; private set; } = false;

    // Change the upper gate.
    public void SetHighGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change the lower gate.
    public void SetLowGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change water level.
    public void SetWaterLevel(WaterLevel newLevel)
    {
        throw new NotImplementedException();
    }

    public override string ToString() =>
        $"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
        $"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
        $"The water level is {CanalLockWaterLevel}.";
}

Az előző kód inicializálja az objektumot, így mindkét kapu zárva van, és a vízszint alacsony. Ezután írja be a következő tesztkódot a Main metódusba, hogy útmutatást nyújthasson az osztály első implementációjának létrehozásakor:

// Create a new canal lock:
var canalGate = new CanalLock();

// State should be doors closed, water level low:
Console.WriteLine(canalGate);

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat enters lock from lower gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");

canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");
Console.WriteLine("Boat enters lock from upper gate");

canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");

canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

Ezután adja hozzá az osztály egyes metódusainak első implementációját CanalLock . Az alábbi kód az osztály módszereit implementálja a biztonsági szabályok betartása nélkül. Biztonsági teszteket később fog hozzáadni:

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = open;
}

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = open;
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = newLevel;
}

Az eddig írt tesztek sikeresek. Implementálta az alapokat. Most írjon egy tesztet az első hibafeltételhez. Az előző tesztek végén mindkét kapu zárva van, és a vízszint alacsonyra van állítva. Adjon hozzá egy tesztet a felső kapu megnyitásához:

Console.WriteLine("=============================================");
Console.WriteLine("     Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
    canalGate = new CanalLock();
    canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");

Ez a teszt meghiúsul, mert megnyílik a kapu. Első implementációként a következő kóddal kijavíthatja:

// Change the upper gate.
public void SetHighGate(bool open)
{
    if (open && (CanalLockWaterLevel == WaterLevel.High))
        HighWaterGateOpen = true;
    else if (open && (CanalLockWaterLevel == WaterLevel.Low))
        throw new InvalidOperationException("Cannot open high gate when the water is low");
}

A tesztek sikeresek. Ha azonban további teszteket ad hozzá, egyre több if záradékot fog hozzáadni, és különböző tulajdonságokat tesztel. Ezek a módszerek hamarosan túl bonyolulttá válik, amikor további feltételes feltételeket ad hozzá.

A parancsok implementálása mintákkal

Jobb módszer, ha mintákkal állapítja meg, hogy az objektum érvényes állapotban van-e egy parancs végrehajtásához. Kifejezheti, ha egy parancs három változó függvényeként engedélyezett: a kapu állapota, a víz szintje és az új beállítás:

Új beállítás Kapu állapota Vízszint Eredmény
Lezárva Lezárva Magas Lezárva
Lezárva Lezárva Alacsony Lezárva
Lezárva Nyit Magas Lezárva
Zárt Nyit Alacsony Zárt
Nyit Lezárva Magas Nyit
Nyit Lezárva Alacsony Lezárva (hiba)
Nyit Nyit Magas Nyit
Nyit Nyit Alacsony Lezárva (hiba)

A táblázat negyedik és utolsó sora átüti a szöveget, mert érvénytelenek. A most hozzáadott kódnak meg kell győződnie arról, hogy a magas víz kapuja soha nem nyílik meg, ha a víz alacsony. Ezek az állapotok egyetlen kapcsolókifejezésként kódolhatók (ne feledje, hogy a false "Zárt" értékre utal):

HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
    (false, false, WaterLevel.High) => false,
    (false, false, WaterLevel.Low) => false,
    (false, true, WaterLevel.High) => false,
    (false, true, WaterLevel.Low) => false, // should never happen
    (true, false, WaterLevel.High) => true,
    (true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
    (true, true, WaterLevel.High) => true,
    (true, true, WaterLevel.Low) => false, // should never happen
};

Próbálja ki ezt a verziót. A tesztek sikeresek, és érvényesítik a kódot. A teljes táblázat a bemenetek és eredmények lehetséges kombinációit mutatja. Ez azt jelenti, hogy Ön és más fejlesztők gyorsan áttekinthetik a táblázatot, és láthatják, hogy az összes lehetséges bemenetet lefedte. Még egyszerűbb, a fordító is segíthet. Az előző kód hozzáadása után láthatja, hogy a fordító figyelmeztetést hoz létre: a CS8524 azt jelzi, hogy a kapcsolókifejezés nem fedi le az összes lehetséges bemenetet. A figyelmeztetés oka az, hogy az egyik bemenet egy enum típus. A fordító az "összes lehetséges bemenetet" az alapul szolgáló típus összes bemeneteként értelmezi, általában egy int. Ez a switch kifejezés csak a deklarált értékeket ellenőrzi a enum. A figyelmeztetés eltávolításához hozzáadhat egy catch-all elvetési mintát a kifejezés utolsó karjához. Ez a feltétel kivételt eredményez, mert érvénytelen bemenetet jelez:

_  => throw new InvalidOperationException("Invalid internal state"),

Az előző kapcsolókarnak az utolsónak kell lennie a switch kifejezésben, mert megfelel az összes bemenetnek. Kísérletezzen úgy, hogy korábban áthelyezi a sorrendbe. Ez CS8510-et eredményez egy fordítóhiba miatt, mert nem érhető el kód egy mintában. A kapcsolókifejezések természetes struktúrája lehetővé teszi a fordító számára, hogy hibákat és figyelmeztetéseket generáljon a lehetséges hibákra. A fordító "biztonsági háló" megkönnyíti a megfelelő kód létrehozását kevesebb iterációban, valamint a kapcsolókarok helyettesítő karakterekkel való kombinálásának szabadságát. A fordító hibát ad ki, ha a kombináció elérhetetlen karokat eredményez, amelyeket nem várt, és figyelmeztetést küld, ha eltávolít egy szükséges kart.

Az első változás az összes kar egyesítése, ahol a parancs a kapu bezárása; ez mindig engedélyezett. Adja hozzá a következő kódot a kapcsolókifejezés első karjaként:

(false, _, _) => false,

Miután hozzáadta az előző kapcsolókart, négy fordítóhibát fog kapni, egyet azon karokon, amelyeken a parancs található false. Ezeket a karokat már az újonnan hozzáadott kar fedi. Ezt a négy sort biztonságosan eltávolíthatja. Ezt az új kapcsolókart úgy tervezték, hogy lecserélje ezeket a feltételeket.

Ezután egyszerűsítheti a négy kart, ahol a parancs a kapu megnyitása. Mindkét esetben, ha a vízszint magas, a kaput meg lehet nyitni. (Az egyikben már nyitva van.) Az egyik eset, amikor a vízszint alacsony, kivételt jelent, a másiknak pedig nem szabad megtörténnie. Ha a vízzár már érvénytelen állapotban van, biztonságosan ki kell dobni ugyanezt a kivételt. Az alábbi egyszerűsítéseket végezheti el ezekre a fegyverekre vonatkozóan:

(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),

Futtassa újra a teszteket, és azok átmennek. A metódus végleges verziója a SetHighGate következő:

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _,    _)               => false,
        (true, _,     WaterLevel.High) => true,
        (true, false, WaterLevel.Low)  => throw new InvalidOperationException("Cannot open high gate when the water is low"),
        _                              => throw new InvalidOperationException("Invalid internal state"),
    };
}

Minták implementálása saját maga

Most, hogy megismerte a technikát, töltse ki magát a módszereket és SetWaterLevel a SetLowGate módszereket. Első lépésként adja hozzá a következő kódot az érvénytelen műveletek teszteléséhez ezeken a metódusokon:

Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetLowGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetHighGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");

Futtassa újra az alkalmazást. Láthatja, hogy az új tesztek sikertelenek, és a csatornazár érvénytelen állapotba kerül. Próbálja meg saját maga implementálni a többi metódust. Az alsó kapu beállítására szolgáló módszernek hasonlónak kell lennie a felső kapu beállításához. A vízszintet megváltoztató módszer különböző ellenőrzéseket végez, de hasonló szerkezetet kell követnie. Hasznos lehet, ha ugyanazt a folyamatot használja a vízszintet állító módszerhez. Kezdje mind a négy bemenettel: Mindkét kapu állapota, a vízszint aktuális állapota és a kért új vízszint. A kapcsolókifejezésnek a következővel kell kezdődnie:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // elided
};

Összesen 16 kapcsolókart kell kitöltenie. Ezután tesztelje és egyszerűsítse le.

Csináltál ehhez hasonló módszereket?

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _, _) => false,
        (true, _, WaterLevel.Low) => true,
        (true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
    {
        (WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
        (WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
        (WaterLevel.Low, _, false, false) => WaterLevel.Low,
        (WaterLevel.High, _, false, false) => WaterLevel.High,
        (WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower water when the high gate is open"),
        (WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise water when the low gate is open"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

A teszteknek át kell mennie, és a csatornazárnak biztonságosan kell működnie.

Összegzés

Ebben az oktatóanyagban megtanulta, hogyan használhatja a mintaegyezést egy objektum belső állapotának ellenőrzésére, mielőtt bármilyen módosítást alkalmaz az adott állapotra. A tulajdonságok kombinációit ellenőrizheti. Miután táblákat készített bármelyik áttűnéshez, tesztelje a kódot, majd egyszerűsítse az olvashatóságot és a karbantarthatóságot. Ezek a kezdeti újrabontások további újrabontásokat javasolhatnak, amelyek ellenőrzik a belső állapotot, vagy más API-módosításokat kezelnek. Ez az oktatóanyag az osztályokat és objektumokat egy adatorientáltabb, mintaalapú megközelítéssel kombinálta az osztályok implementálásához.