Použití porovnávání vzorů k vytvoření chování třídy pro lepší kód

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

Budete muset nastavit počítač tak, aby běžel .NET. Stáhněte si Sadu Visual Studio 2022 nebo sadu .NET SDK.

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. Zámek má dvě brány 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, změní se hladina vody 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ě. Sejf ty 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, 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ší a další if klauzule a otestujete různé vlastnosti. Brzy budou tyto metody příliš složité, protože přidáváte další podmíněné podmínky.

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í úroveň Výsledek
Uzavřené Uzavřené Vysoká Uzavřené
Uzavřené Uzavřené Nízká Uzavřené
Uzavřené Otevření Vysoká Uzavřené
Zavřeno Otevřená Nízké Zavřeno
Otevřené Zavřeno Vysoká Otevření
Otevřené Zavřeno Nízká Zavřeno (chyba)
Otevření Otevření Vysoká Otevření
Otevřená Otevřená Nízké Zavřeno (chyba)

Čtvrté a poslední řádky v tabulce přeškrtly 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 výraz s jedním přepínačem (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. Testy projdou ověřením kódu. 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. Ještě jednodušší může kompilátor pomoct. 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 vzor zahození zachytávání pro poslední rameno 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á chyby v případě, že kombinace způsobí nedostupné zbraně, 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í arm:

(false, _, _) => false,

Po přidání předchozí arm přepínače se zobrazí čtyři chyby kompilátoru, jeden na každé z paží, kde je falsepříkaz . Tyto zbraně jsou již pokryty nově přidanou paží. Tyto čtyři řádky můžete bezpečně odebrat. Chtěli jste, aby tato nová ramena přepínače nahradila tyto podmínky.

Dále můžete zjednodušit čtyři zbraně, kde je příkaz otevřít bránu. V oboupřípadechch (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 stejnou 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"),
    };
}

Implementace vzorů sami

Teď, když jste viděli techniku, vyplňte metody SetLowGate a SetWaterLevel metody sami. 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. Může být užitečné použít stejný postup pro metodu, která nastaví hladinu vody. 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
};

Budete mít 16 celkových přepínacích ramen k vyplnění. Pak otestujte a zjednodušte.

Udělali jste podobné metody?

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

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.