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 |
Nyit | Lezárva | Magas | Nyit |
Nyit | Lezárva | Alacsony | Lezárva (hiba) |
Nyit | Nyit | Magas | Nyit |
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.