Delen via


Patroonkoppeling gebruiken om uw klassegedrag te bouwen voor betere code

De patroonkoppelingsfuncties in C# bieden syntaxis om uw algoritmen uit te drukken. U kunt deze technieken gebruiken om het gedrag in uw klassen te implementeren. U kunt objectgeoriënteerd klasseontwerp combineren met een gegevensgeoriënteerde implementatie om beknopte code te bieden tijdens het modelleren van echte objecten.

In deze zelfstudie leert u het volgende:

  • Uw objectgeoriënteerde klassen uitdrukken met behulp van gegevenspatronen.
  • Implementeer deze patronen met behulp van de patroonkoppelingsfuncties van C#.
  • Gebruik diagnostische compilergegevens om uw implementatie te valideren.

Voorwaarden

Een simulatie van een kanaalslot bouwen

In deze zelfstudie bouwt u een C#-klasse die een kanaalvergrendeling simuleert. Kort gezegd, een kanaalslot is een apparaat dat boten ophekt en verlaagt tijdens het reizen tussen twee stukken water op verschillende niveaus. Een slot heeft twee poorten en een mechanisme om het waterniveau te wijzigen.

Bij de normale werking gaat een boot een van de poorten binnen terwijl het waterniveau in de sluis overeenkomt met het waterniveau aan de kant waar de boot binnenkomt. Eenmaal in het slot wordt het waterniveau aangepast aan het waterniveau waar de boot het slot verlaat. Zodra het waterniveau aan die kant overeenkomt, wordt de poort aan de uitgangszijde geopend. Veiligheidsmaatregelen zorgen ervoor dat een operator geen gevaarlijke situatie in het kanaal kan creëren. Het waterniveau kan alleen worden gewijzigd wanneer beide poorten gesloten zijn. Maximaal één poort kan open zijn. Om een poort te openen, moet het waterniveau in de slot overeenkomen met het waterniveau buiten de poort die wordt geopend.

U kunt een C#-klasse bouwen om dit gedrag te modelleren. Een CanalLock-klasse ondersteunt opdrachten om een poort te openen of te sluiten. Het zou andere opdrachten hebben om het water te verhogen of te verlagen. De klasse moet ook eigenschappen ondersteunen om de huidige toestand van zowel poorten als het waterniveau te lezen. Uw methoden implementeren de veiligheidsmaatregelen.

Een klasse definiëren

U bouwt een consoletoepassing om uw CanalLock-klasse te testen. Maak een nieuw consoleproject voor .NET 5 met Visual Studio of de .NET CLI. Voeg vervolgens een nieuwe klasse toe en noem deze CanalLock. Ontwerp vervolgens uw openbare API, maar laat de methoden niet geïmplementeerd:

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

Met de voorgaande code wordt het object geïnitialiseerd, zodat beide poorten worden gesloten en het waterniveau laag is. Schrijf vervolgens de volgende testcode in uw Main methode om u te begeleiden bij het maken van een eerste implementatie van de klasse:

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

Voeg vervolgens een eerste implementatie van elke methode toe in de CanalLock klasse. Met de volgende code worden de methoden van de klasse geïmplementeerd zonder dat u zich zorgen hoeft te maken over de veiligheidsregels. U voegt later veiligheidstests toe:

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

De tests die u tot nu toe hebt geschreven, zijn geslaagd. U hebt de basisbeginselen geïmplementeerd. Schrijf nu een test voor de eerste foutvoorwaarde. Aan het einde van de vorige tests zijn beide poorten gesloten en is het waterniveau ingesteld op laag. Voeg een test toe om de bovenste poort te openen:

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

Deze test mislukt omdat de poort wordt geopend. Als eerste implementatie kunt u deze oplossen met de volgende code:

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

Uw tests zijn geslaagd. Maar naarmate u meer tests toevoegt, voegt u meer if componenten toe en test u verschillende eigenschappen. Deze methoden worden binnenkort te ingewikkeld naarmate u meer voorwaarden toevoegt.

De opdrachten implementeren met patronen

Een betere manier is om patronen te gebruiken om te bepalen of het object een geldige status heeft om een opdracht uit te voeren. U kunt uitdrukken als een opdracht is toegestaan als een functie van drie variabelen: de status van de poort, het niveau van het water en de nieuwe instelling:

Nieuwe instelling Poortstatus Waterstand Resultaat
Gesloten Gesloten Hoog Gesloten
Gesloten Gesloten Laag Gesloten
Gesloten Openen Hoog Gesloten
gesloten Openen laag gesloten
Openen Gesloten Hoog Openen
Openen Gesloten Laag Gesloten (Fout)
Openen Openen Hoog Openen
Openen Openen laag gesloten (fout)

De vierde en laatste rijen in de tabel hebben doorstreepte tekst omdat de tekst ongeldig is. De code die u nu toevoegt, moet ervoor zorgen dat de hoge waterpoort nooit wordt geopend wanneer het water laag is. Deze statussen kunnen worden gecodeerd als één switchexpressie (houd er rekening mee dat false 'Gesloten' aangeeft):

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

Probeer deze versie. Uw tests slagen, waardoor de code gevalideerd wordt. In de volledige tabel ziet u de mogelijke combinaties van invoer en resultaten. Dat betekent dat u en andere ontwikkelaars snel naar de tabel kunnen kijken en zien dat u alle mogelijke invoer hebt behandeld. Nog eenvoudiger, de compiler kan ook helpen. Nadat u de vorige code hebt toegevoegd, ziet u dat de compiler een waarschuwing genereert: CS8524 geeft aan dat de switchexpressie niet alle mogelijke invoer omvat. De reden voor deze waarschuwing is dat een van de invoerwaarden een enum type is. De compiler interpreteert 'alle mogelijke invoer' als alle invoer van het onderliggende type, meestal een int. Deze switch expressie controleert alleen de waarden die zijn gedeclareerd in de enum. Als u de waarschuwing wilt verwijderen, kunt u een catch-all-verwijderingspatroon toevoegen voor de laatste arm van de expressie. Deze voorwaarde genereert een uitzondering, omdat deze ongeldige invoer aangeeft:

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

De voorgaande schakelarm moet voor het laatst in de switch expressie staan, omdat deze overeenkomt met alle invoerwaarden. Experimenteer door het eerder in de volgorde te verplaatsen. Dit veroorzaakt een compilerfout CS8510 voor onbereikbare code in een patroon. Door de natuurlijke structuur van switch-expressies kan de compiler fouten en waarschuwingen genereren voor mogelijke vergissingen. Met de compiler 'safety net' kunt u eenvoudiger juiste code maken in minder iteraties en de vrijheid om schakelarmen met jokertekens te combineren. De compiler veroorzaakt fouten als uw combinatie resulteert in onbereikbare armen die u niet had verwacht en waarschuwingen als u een benodigde arm verwijdert.

De eerste wijziging is het combineren van alle armen waar de opdracht is om de poort te sluiten; dat is altijd toegestaan. Voeg de volgende code toe als de eerste arm in uw switchexpressie:

(false, _, _) => false,

Nadat u de vorige schakelarm hebt toegevoegd, krijgt u vier compilerfouten, één op elk van de armen waar de opdracht is false. Die armen zijn al bedekt door de nieuw toegevoegde arm. U kunt deze vier regels veilig verwijderen. U bent van plan dat deze nieuwe schakelarm die voorwaarden vervangt.

Vervolgens kunt u de vier armen vereenvoudigen waar het commando is om de poort te openen. In beide gevallen waar het waterniveau hoog is, kan de poort worden geopend. (In een ervan is het al geopend.) Een situatie waarin het waterniveau laag is, roept een uitzondering op en de andere zou niet mogen voorkomen. Het moet veilig zijn om dezelfde uitzondering te gooien als de watervergrendeling al een ongeldige status heeft. U kunt de volgende vereenvoudigingen voor deze armen maken:

(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"),

Voer je tests opnieuw uit en ze zijn geslaagd. Dit is de laatste versie van de methode SetHighGate:

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

Patronen zelf implementeren

Nu u de techniek hebt gezien, vult u zelf de SetLowGate en SetWaterLevel methoden in. Voeg eerst de volgende code toe om ongeldige bewerkingen op deze methoden te testen:

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

Voer uw toepassing opnieuw uit. U kunt zien dat de nieuwe tests mislukken en dat de kanaalvergrendeling een ongeldige status krijgt. Probeer zelf de resterende methoden te implementeren. De methode voor het instellen van de onderste poort moet vergelijkbaar zijn met de methode om de bovenste poort in te stellen. De methode waarmee het waterniveau verandert, heeft verschillende controles, maar moet een vergelijkbare structuur volgen. Het kan handig zijn om hetzelfde proces te gebruiken voor de methode waarmee het waterniveau wordt ingesteld. Begin met alle vier de ingangen: de toestand van beide poorten, de huidige toestand van het waterniveau en het aangevraagde nieuwe waterniveau. De switchexpressie moet beginnen met:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // elided
};

U hebt in totaal 16 schakelarmen die ingevuld moeten worden. Test en vereenvoudig vervolgens.

Heb je methoden op deze manier gemaakt?

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

Uw tests moeten slagen en de sluis moet veilig functioneren.

Samenvatting

In deze zelfstudie hebt u geleerd om patroonkoppeling te gebruiken om de interne status van een object te controleren voordat u wijzigingen toepast op die status. U kunt combinaties van eigenschappen controleren. Zodra u tabellen voor een van deze overgangen hebt gemaakt, test u uw code en vereenvoudigt u de leesbaarheid en onderhoudbaarheid. Deze initiële herstructureringen kunnen verdere herstructureringen voorstellen die de interne status valideren of andere API-wijzigingen beheren. In deze zelfstudie zijn klassen en objecten gecombineerd met een meer gegevensgeoriënteerde, patroongebaseerde benadering om deze klassen te implementeren.