Oktatóanyag: Mintailesztés alapjai

Tip

Új szoftverfejlesztés? Először az Első lépések oktatóanyagokkal kezdje. Osztályokat, metódusokat és vezérlési folyamatot vezetnek be.

Tapasztalt egy másik nyelven? Ez az oktatóanyag bemutatja, hogy a C#-minták hogyan fejezhetik ki egyértelműen az objektum viselkedését, ha a szabályok az objektum aktuális állapotától függenek.

Ebben az oktatóanyagban egy konzolalkalmazást hoz létre, amely a csatornazsilipre vonatkozó szabályokat modellezi.

A zsilip két különböző magasságú vízszakasz között emeli vagy csökkenti a hajókat. Két kapuval és egy kamrával rendelkezik, amelynek vízszintje alacsony és magas beállítás között változik. A zsilip csak akkor működik biztonságosan, ha a vízszint és a kapu helyzete megfelelő kombinációban van.

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

  • objektum viselkedésének kifejezése állapot szerinti mintaillesztéssel.
  • A szabályok megvalósítása C# mintailesztéssel.
  • A implementáció ellenőrzéséhez használjon fordítódiagnosztikát.

Prerequisites

Telepítési utasítások

Ez a WinGet konfigurációs fájl használható a Windows rendszerben az összes előfeltétel telepítéséhez. Ha már telepített valamit, a WinGet kihagyja ezt a lépést.

  1. Töltse le a fájlt, és kattintson rá duplán a futtatáshoz.
  2. Olvassa el a licencszerződést, írja be y, és válassza Adja meg, amikor a rendszer kéri az elfogadást.
  3. Ha a tálcán megjelenik egy villogó Felhasználói fiókok felügyelete (UAC) kérés, engedélyezze a telepítés folytatását.

Más platformokon külön kell telepítenie ezeket az összetevőket.

  1. Töltse le az ajánlott telepítőt a .NET SDK letöltési oldaláról és kattintson rá duplán a futtatásához. A letöltési oldal észleli a platformot, és a legújabb telepítőt javasolja a platformhoz.
  2. Töltse le a legújabb telepítőt a Visual Studio Code kezdőlapjáról, és kattintson duplán a futtatáshoz. Ez az oldal a platformot is észleli, és a hivatkozásnak helyesnek kell lennie a rendszer számára.
  3. Kattintson a "Telepítés" gombra a C# DevKit bővítménylapon. Ez megnyitja a Visual Studio-kódot, és megkérdezi, hogy szeretné-e telepíteni vagy engedélyezni a bővítményt. Válassza a "telepítés" lehetőséget.

Zsilip szimulációjának létrehozása

A zsilip emeli és süllyeszti a hajókat a különböző szintű vízi utak között. Ebben az oktatóanyagban a szimulált zsilip alsó kapuval, felső kapuval és alacsony vagy magas vízzel rendelkezik.

Normál működés esetén egy hajó akkor lép be, amikor a zsilipen belüli vízszint megegyezik a bejárati oldalon lévő szinttel. Amint a hajó beér, mindkét kapu bezárul. A vízszint a kimeneti oldalnak megfelelően változik, majd megnyílik a kilépési kapu. A modell biztonsága érdekében a vízszint csak akkor változhat, ha mindkét kapu zárva van, és egy kapu csak akkor nyílhat ki, ha a vízszint megegyezik az adott oldali szinttel.

Ezeket a szabályokat egy CanalLock osztállyal is modellezheti. Parancsokat tesz elérhetővé bármelyik kapu megnyitásához vagy bezárásához, valamint a víz felemeléséhez vagy csökkentéséhez. Emellett olyan tulajdonságokat is elérhetővé tesz, amelyek a zárolás aktuális állapotát jelzik.

Definiálja az osztályt

Hozzon létre egy konzolprojektet, majd vegyen fel egy CanalLock nevű osztályt. Kezdje a nyilvános API tervezésével, és hagyja a metódusokat implementálatlanul:

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 úgy inicializálja a zsilipet, hogy mindkét kapu zárva van, és a vízszint alacsony. Ezután az első implementáció irányításához adja hozzá a következő kódot a(z) Main elemhez:

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

Most adjon hozzá egy első implementációt, amely a biztonsági szabályok kényszerítése nélkül módosítja az egyes állapotértékeket:

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

Ezek az első ellenőrzések sikeresek. A mechanika működik. Ezután adjon hozzá egy tesztet az első hibafeltételhez. Az előző szekvencia végén mindkét kapu zárva volt, és a vízszint alacsony. Próbálja meg megnyitni a felső kaput:

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

A teszt meghiúsul, mert a felső kapu akkor nyílik meg, amikor nem kellene. Az első javítás a következőképpen nézhet ki:

// 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 ismét sikeresek. De ahogy további feltételeket ad hozzá, több if utasítást halmoz fel. A kód vizsgálata nehezebbé válik, mert minden szabály külön van a többitől.

Valósítsa meg a parancsokat minták használatával

Az egyértelműbb megoldás az, ha mintákkal írja le közvetlenül az érvényes kombinációkat. A következő lépésben minden kapcsolókifejezés egy többest használ a minta bemeneteként. A C# egyszer kiértékeli ezt a tuple-t, és minden switch-ág ellenőrzi az aktuális kapu állapotát, a vízszintet és a kért új beállítást.

A felső kapu esetében az alábbi kombinációkat foglalhatja össze:

Új beállítás Kapu állapota Vízszint Result
Zárva Zárva Magas Zárva
Zárva Zárva Alacsony Zárva
Zárva Open Magas Zárva
Zárt Nyisd meg Low Zárt
Open Zárva Magas Open
Open Zárva Alacsony Bezárva (hiba)
Open Open Magas Open
Nyisd meg Nyisd meg Low Lezárva (hiba)

Az áthúzott sorok érvénytelen belső állapotokat jelölnek. A switch kifejezés közvetlenül kódolhatja az érvényes átmeneteket. false még mindig azt jelenti, hogy a kapu zárva van:

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. A fordító arra is figyelmeztet, hogy a kapcsolókifejezés nem teljes – nem fedi le az összes lehetséges értéket –, mert WaterLevel egy enum, és a C# lehetővé teszi, hogy az enum alapjául szolgáló numerikus típus bármely értéke azzá az enummá legyen konvertálva, még olyan is, amelyhez nem tartozik névvel ellátott tag. Adjon hozzá egy végső kart az elvetési mintával (_) a lehetetlen belső állapotok kezeléséhez:

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

Az ágnak utolsónak kell lennie, mert az elvetési minta megegyezik minden fennmaradó bemenettel.

Ezután egyszerűsítheti a korábbi ágakat. A kapu bezárása mindig engedélyezett, így egy ág helyettesítheti a négy különálló lezárt állapotot:

(false, _, _) => false,

Az érvényes nyitott eseteket is kombinálhatja, és megőrizheti az egy meglévő biztonsági hibát:

(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 programot. A tesztek még mindig sikeresek. Íme a végső SetHighGate megvalósítás:

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

A fennmaradó szabályok implementálása

Most alkalmazza ugyanezt az ötletet a következőre SetLowGate : és SetWaterLevel. Első lépésként adjon hozzá érvénytelen műveleteket feltáró teszteket:

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. Ezek a tesztek sikertelenek, és a zsilip érvénytelen állapotot ér el. Implementálja a fennmaradó metódusokat a teljes állapotra való illesztéssel. SetLowGate hasonló a SetHighGate. SetWaterLevel az aktuális vízszintet és mindkét kapupozíciót használja:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // arms go here
};

16 kombinációt kell figyelembe vennie. Kezdje a teljes táblázattal, írja meg a switch ágakat, futtassa a teszteket, majd egyszerűsítse az ismétlődő eredményeket.

Végül ezekhez hasonló módszerekre jutott?

// 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 most át kell mennie, és a zsilipnek érvényesítenie kell a biztonsági szabályait.

Összegzés

Ebben az oktatóanyagban mintailesztést használt, hogy kifejezze, hogyan válthat egy objektum egyik érvényes állapotból egy másik érvényes állapotba. A minták együtt tartották az engedélyezett átmeneteket, hogy könnyebben összehasonlíthassa őket, mint egy hosszú elágaztatási utasítássorozattal. Ez a megközelítés akkor működik jól, ha a viselkedés több állapotérték együttes mintázatától függ.