Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Funkce porovnávání vzorů v jazyce C# poskytují syntaxi pro vyjádření algoritmů. Tyto techniky můžete použít k implementaci chování ve třídách. Návrh třídy orientované na objekty můžete kombinovat s implementací zaměřenou na data, která poskytuje stručný kód při modelování reálných objektů.
V tomto kurzu se naučíte:
- Vyjádřete objektově orientované třídy pomocí vzorů dat.
- Tyto vzory implementujte pomocí funkcí porovnávání vzorů jazyka C#.
- Využijte diagnostiku kompilátoru k ověření implementace.
Požadavky
- Nejnovější sada .NET SDK
- editor Visual Studio Code editoru
- C# DevKit
Vytvoření simulace zámku kanálu
V tomto kurzu vytvoříte třídu jazyka C#, která simuluje zámek kanálu. Stručně řečeno, zámek kanálu je zařízení, které zvyšuje a snižuje lodě při cestování mezi dvěma úseky vody na různých úrovních. Zdymadlo má dvě vrata a nějaký mechanismus pro změnu hladiny vody.
V normálním provozu loď vstoupí do jedné z bran, zatímco hladina vody v zámku odpovídá hladině vody na straně, do které loď vstoupí. Jakmile je v zámku, úroveň vody se změní tak, aby odpovídala hladině vody, kde loď opustí zámek. Jakmile úroveň vody odpovídá této straně, otevře se brána na výstupní straně. Bezpečnostní opatření zajišťují, aby provozovatel nemohl vytvořit nebezpečnou situaci v kanálu. Hladinu vody lze změnit pouze v případech, kdy jsou obě brány uzavřeny. Nejméně jedna brána může být otevřená. Aby bylo možné otevřít bránu, musí hladina vody v zámku odpovídat hladině vody mimo bránu, která se otevírá.
Můžete vytvořit třídu jazyka C#, která bude modelovat toto chování. Třída CanalLock podporuje příkazy pro otevření nebo zavření brány. Mělo by to další příkazy ke zvýšení nebo snížení vody. Třída by také měla podporovat vlastnosti ke čtení aktuálního stavu bran i hladiny vody. Vaše metody implementují bezpečnostní opatření.
Definování třídy
Sestavíte konzolovou aplikaci pro otestování třídy CanalLock . Vytvořte nový projekt konzoly pro .NET 5 pomocí sady Visual Studio nebo rozhraní příkazového řádku .NET. Pak přidejte novou třídu a pojmenujte ji CanalLock. Dále navrhněte veřejné rozhraní API, ale ponechte metody, které nejsou implementované:
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}.";
}
Předchozí kód inicializuje objekt tak, aby obě brány byly uzavřeny a hladina vody je nízká. Dále do své Main metody napište následující testovací kód, který vás provede vytvořením první implementace třídy:
// 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}");
Dále přidejte první implementaci každé metody ve CanalLock třídě. Následující kód implementuje metody třídy bez obav z bezpečnostních pravidel. Bezpečnostní testy přidáte později:
// 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;
}
Testy, které jste zatím napsali, úspěšně prošly. Implementovali jste základy. Teď napište test pro první podmínku selhání. Na konci předchozích testů jsou obě brány uzavřeny a hladina vody je nastavena na nízkou. Přidejte test a zkuste otevřít horní bránu:
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}");
Tento test selže, protože brána se otevře. Jako první implementaci byste ji mohli opravit následujícím kódem:
// 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");
}
Vaše testy jsou úspěšné. Když ale přidáte další testy, přidáte další if klauzule a otestujete různé vlastnosti. Brzy se tyto metody při přidávání dalších podmíněných podmínek příliš zkomplikují.
Implementace příkazů se vzory
Lepší způsob je použít vzory k určení, zda je objekt v platném stavu ke spuštění příkazu. Můžete vyjádřit, jestli je příkaz povolený jako funkce tří proměnných: stav brány, úroveň vody a nové nastavení:
| Nové nastavení | Stav brány | Vodní hladina | Výsledek |
|---|---|---|---|
| Zavřeno | Zavřeno | Vysoko | Zavřeno |
| Zavřeno | Zavřeno | Nízké | Zavřeno |
| Zavřeno | Otevřít | Vysoko | Zavřeno |
|
|
|
|
|
| Otevřít | Zavřeno | Vysoko | Otevřít |
| Otevřít | Zavřeno | Nízké | Zavřeno (chyba) |
| Otevřít | Otevřít | Vysoko | Otevřít |
|
|
|
|
|
Čtvrtý a poslední řádek v tabulce mají přeškrtnutý text, protože jsou neplatné. Kód, který přidáváte, by měl zajistit, aby se brána vysoké vody nikdy neotevírala, když je voda nízká. Tyto stavy mohou být kódovány jako jednoduchý výraz přepínače (nezapomeňte, že false označuje "Uzavřeno"):
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
};
Zkuste tuto verzi. Vaše testy projdou, což ověřuje kód. V celé tabulce jsou uvedeny možné kombinace vstupů a výsledků. To znamená, že vy i ostatní vývojáři se můžete rychle podívat na tabulku a zjistit, že jste probrali všechny možné vstupy. Kompilátor může také pomoci a tím je to ještě jednodušší. Po přidání předchozího kódu uvidíte, že kompilátor vygeneruje upozornění: CS8524 indikuje, že výraz přepínače nepokrývá všechny možné vstupy. Důvodem tohoto upozornění je, že jedním ze vstupů je enum typ. Kompilátor interpretuje "všechny možné vstupy" jako všechny vstupy ze základního typu, obvykle .int Tento switch výraz kontroluje pouze hodnoty deklarované v objektu enum. Pokud chcete upozornění odebrat, můžete přidat zahozený vzor pro poslední část výrazu. Tato podmínka vyvolá výjimku, protože označuje neplatný vstup:
_ => throw new InvalidOperationException("Invalid internal state"),
Předchozí rameno přepínače musí být ve výrazu switch poslední, protože odpovídá všem vstupům. Experimentujte tím, že ho přesunete dříve v pořadí. To způsobí chybu kompilátoru CS8510 pro nedostupný kód ve vzoru. Přirozená struktura výrazů přepínače umožňuje kompilátoru generovat chyby a upozornění pro možné chyby. Kompilátor "safety net" usnadňuje vytvoření správného kódu v menším počtu iterací a volnost kombinování přepínacích ramen se zástupnými čísly. Kompilátor vydává chyby, pokud kombinace vede k nedostupným ramenům, které jste neočekávala, a upozornění, pokud odeberete potřebnou ruku.
První změnou je kombinovat všechny zbraně, ve kterých je příkaz zavřít bránu; to je vždycky povolené. Do výrazu switch přidejte následující kód jako první větev:
(false, _, _) => false,
Po přidání předchozí větve přepínače se zobrazí čtyři chyby kompilátoru, jedna na každé z větví, kde je příkaz false. Tyto paže jsou již pokryty nově přidanou paží. Tyto čtyři řádky můžete bezpečně odebrat. Chtěli jste, aby nové rameno přepínače nahradilo tyto podmínky.
Dále můžete zjednodušit čtyři paže, kde je příkaz otevřít bránu. V obou případech, kdy je hladina vody vysoká, může být brána otevřena. (V jednom už je otevřený.) Jeden případ, kdy je hladina vody nízká, vyvolá výjimku a druhý by se neměl stát. Pokud je zámek vody již v neplatném stavu, mělo by být bezpečné vyvolat tutéž výjimku. Pro tyto zbraně můžete provést následující zjednodušení:
(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"),
Znovu spusťte testy a projdou. Tady je konečná verze SetHighGate metody:
// 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"),
};
}
Připravte vzory sami
Teď, když jste viděli techniku, vyplňte sami metody SetLowGate a SetWaterLevel. Začněte přidáním následujícího kódu, který otestuje neplatné operace s těmito metodami:
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}");
Spusťte aplikaci znovu. Uvidíte, že nové testy selžou a zámek kanálu se dostane do neplatného stavu. Zkuste implementovat zbývající metody sami. Metoda nastavení dolní brány by měla být podobná metodě, která nastaví horní bránu. Metoda, která mění hladinu vody, má různé kontroly, ale měla by dodržovat podobnou strukturu. Pro metodu, která nastavuje hladinu vody, může být užitečné použít stejný postup. Začněte se všemi čtyřmi vstupy: stav obou bran, aktuální stav hladiny vody a požadovanou novou hladinu vody. Výraz přepínače by měl začínat na:
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
// elided
};
Máte celkem 16 přepínacích ramen k doplnění. Pak otestujte a zjednodušte.
Vytvořili jste metody jako tyto?
// 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"),
};
}
Testy by měly proběhnout a zámek kanálu by měl bezpečně fungovat.
Shrnutí
V tomto kurzu jste se naučili použít porovnávání vzorů ke kontrole interního stavu objektu před použitím jakýchkoli změn v tomto stavu. Můžete zkontrolovat kombinace vlastností. Jakmile vytvoříte tabulky pro některý z těchto přechodů, otestujete kód a zjednodušíte čitelnost a udržovatelnost. Tyto počáteční refaktoringy můžou navrhovat další refaktoringy, které ověřují interní stav nebo spravují jiné změny rozhraní API. Tento kurz kombinuje třídy a objekty s více datovými orientovanými a vzorovými přístupy k implementaci těchto tříd.