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.

Ebben az oktatóanyagban a következőket sajátíthatja el:

  • 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

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

Ebben az oktatóanyagban egy C#-osztályt hoz létre, amely egy zsilipetszimulá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 kapun, miközben a zsilip vízszintje megegyezik azzal a vízszinttel, amely azon az oldalon van, amelyen a hajó belép. Miután a hajó a zsilipbe került, a vízszint megváltozik, hogy megfeleljen annak a vízszintnek, ahol a hajó elhagyja a zsilipet. Amint a vízszint megegyezik ezen az oldalon, megnyílik a kapu a kilépési oldalon. A biztonsági intézkedések biztosítják, hogy a kezelő 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ámogatja 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 megvalósítják a biztonsági intézkedéseket.

Osztály definiálása

Létrehozhat egy konzolalkalmazást a CanalLock osztály teszteléséhez. 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 egyes metódusok első implementációját a CanalLock osztályhoz. 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 adhat hozzá:

// 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 sikeresen átmennek. Megvalósítottad 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á, további if záradékokat ad hozzá, és teszteli a különböző tulajdonságokat. Ezek a metódusok hamarosan túl bonyolultak lesznek, amikor további feltételes feltételeket ad hozzá.

A parancsok implementálása mintákkal

Jobb módszer, ha mintákat használunk annak megállapítására, 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
Zárt Zárt Magas Zárt
Zárt Zárt Alacsony Zárt
Zárt Nyitott Magas Zárt
Zárt Megnyitás Alacsony Zárt
Nyitott Zárt Magas Nyitott
Nyitott Zárt Alacsony Lezárva (hiba miatt)
Nyitott Nyitott Magas Nyitott
Megnyit Megnyit Alacsony lezárt (hiba)

A táblázat negyedik és utolsó sora áthúzott szöveggel rendelkezik, 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 false a "Zárt" értéket jelöli):

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: CS8524 azt jelzi, hogy a kapcsolókifejezés nem fedi le az összes lehetséges bemenetet. Ennek a figyelmeztetésnek az az oka, 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 enumdeklarált értékeket ellenőrzi. A figyelmeztetés eltávolításához hozzáadhat egy általános 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 utolsónak kell lennie a switch kifejezésben, mert minden bemenetnek megfelel. Próbálja meg korábban elhelyezni a sorrendben. Ez egy CS8510 fordítóhibát okoz a mintában lévő nem elérhető kód miatt. 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ákat jelez, ha a kombináció nem elérhető ágakhoz vezet, amelyeket nem várt, és figyelmeztetést ad, ha eltávolít egy szükséges ágat.

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 mindegyik karon, ahol a parancs falsetalálható. Ezeket a karokat már az újonnan hozzáadott kar fedi. Ezt a négy sort biztonságosan eltávolíthatja. Az Ön szándéka az volt, hogy ez az új kapcsolókar 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 lehet 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 le újra a teszteket, és sikeresek lesznek. A SetHighGate metódus végleges verziója:

// 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"),
    };
}

Készítsd el a mintákat magad

Most, hogy megismerte a technikát, töltse ki a SetLowGate és SetWaterLevel metódusokat. 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 low gate when the water is high"),
        _ => 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.

Összefoglalá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, tesztelheti a kódot, majd egyszerűsítheti 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.