Tillståndsdiagram
Ett tillståndsdiagram (state machine) visar vilka lägen en sak kan vara i, och vad som får den att byta läge. En order kan vara skapad, betald eller skickad. En dörr kan vara öppen, stängd eller låst. Diagrammet svarar på två frågor: vilka tillstånd finns? och vilka övergångar är tillåtna? — och lika viktigt: vilka är inte tillåtna.
När du läst detta ska du kunna
- Känna igen och namnge delarna i ett tillståndsdiagram
- Läsa och skriva en övergång på formen
händelse [villkor] / handling - Rita livscykeln för ett objekt, med självövergång och sluttillstånd
- Implementera ett tillståndsdiagram i C# med
enumoch ett switch-uttryck
Vad används det till?
- Beskriva en livscykel — en order, ett supportärende, en ansökan, ett konto
- Hitta förbjudna övergångar — kan en order som redan är skickad avbrytas? Diagrammet tvingar fram svaret
- Styra beteende i spel och gränssnitt — en spelfigur som står, går, hoppar; en knapp som är aktiv eller inaktiv
- Planera koden — ett tillståndsdiagram blir nästan rad för rad en
enumoch enswitch
Delarna och vad de heter
Exemplet är en kaffeautomat. Den står Redo tills någon stoppar i tillräckligt med pengar. Då brygger den — först maler den, sedan värmer den — och går tillbaka till Redo.
| Del | Hur den ritas | Vad den betyder |
|---|---|---|
| Initialtillstånd (initial state) | Fylld prick med en pil | Var objektet börjar. Det är inget riktigt tillstånd — bara en pekare till det första |
| Tillstånd (state) | Ruta med rundade hörn | Ett läge objektet kan befinna sig i en stund. Namnge med ett adjektiv eller particip: Redo, Betald, Låst |
| Övergång (transition) | Pil från ett tillstånd till ett annat | Objektet byter läge. Allt som inte har en pil är förbjudet |
| Händelse (event, trigger) | Första ordet vid pilen | Det som sätter igång övergången: ett knapptryck, ett metodanrop, att tiden går ut |
| Villkor (guard) | [hakparenteser] efter händelsen | Övergången sker bara om villkoret är sant just då |
| Effekt (effect, action) | / handling sist på raden | Något som utförs under övergången: skicka kvitto, starta en timer |
| Självövergång (self-transition) | Pil som går ut och in i samma tillstånd | Händelsen hanteras, effekten utförs — men tillståndet blir detsamma |
| Sammansatt tillstånd (composite state) | Stor rundad ruta med egna tillstånd inuti | Ett tillstånd som har deltillstånd. Medan automaten Brygger är den antingen i Maler eller i Värmer |
| Sluttillstånd (final state) | Prick med ring runt (“tjuröga”) | Livscykeln är slut. Inga pilar går därifrån |
Hela raden vid en pil läses alltså händelse [villkor] / handling — och alla tre delar är frivilliga. mynt [belopp ≥ 20] / starta betyder: när ett mynt stoppas i, om beloppet då är minst 20 kronor, gör “starta” och byt till Brygger. Saknas händelsen (som vid malt) sker övergången automatiskt när tillståndet är klart.
Inte samma rutor: Ett tillståndsdiagram och ett aktivitetsdiagram har liknande rundade rutor, men betyder olika saker. I ett aktivitetsdiagram är rutan något som görs (“Ta betalt”). I ett tillståndsdiagram är rutan något som objektet är (“Betald”) — och det står kvar där tills en händelse kommer.
Exempel — en beställning
En beställning skapas, betalas, skickas och levereras. Den kan avbrytas så länge den inte har skickats — avbryts den efter betalning ska pengarna tillbaka. Medan paketet är på väg kommer det spårningsuppdateringar, men beställningen stannar i Skickad.
Det intressanta är lika mycket det som saknas: det finns ingen pil från Skickad till Avbruten. En skickad beställning kan alltså inte avbrytas — då får kunden göra en retur istället. Och det finns ingen pil ut från Levererad eller Avbruten: där är livscykeln slut.
Samma diagram i C#. Tillstånden blir en enum, händelserna en annan, och övergångarna ett switch-uttryck på paret (tillstånd, händelse):
public enum OrderState { Created, Paid, Shipped, Delivered, Cancelled }public enum OrderEvent { Pay, Ship, UpdatePosition, Confirm, Cancel }
public class Order{ public OrderState State { get; private set; } = OrderState.Created; // initialtillstånd
public void Handle(OrderEvent ev, bool cardApproved = false) { OrderState next = (State, ev) switch { (OrderState.Created, OrderEvent.Pay) when cardApproved => OrderState.Paid, // betala [kortet godkänt] (OrderState.Created, OrderEvent.Pay) => OrderState.Created, // kortet nekat: stå kvar (OrderState.Paid, OrderEvent.Ship) => OrderState.Shipped, (OrderState.Shipped, OrderEvent.UpdatePosition) => OrderState.Shipped, // självövergång (OrderState.Shipped, OrderEvent.Confirm) => OrderState.Delivered, (OrderState.Created, OrderEvent.Cancel) => OrderState.Cancelled, (OrderState.Paid, OrderEvent.Cancel) => OrderState.Cancelled, _ => throw new InvalidOperationException($"{ev} är inte tillåtet när ordern är {State}.") };
RunEffect(State, next, ev); // effekten: "/ handling" vid pilen State = next; }
private void RunEffect(OrderState from, OrderState to, OrderEvent ev) { switch (from, to) { case (OrderState.Created, OrderState.Paid): SendReceipt(); break; // / skicka kvitto case (OrderState.Paid, OrderState.Shipped): SendTrackingLink(); break; case (OrderState.Shipped, OrderState.Shipped): UpdateTracking(); break; case (OrderState.Paid, OrderState.Cancelled): Refund(); break; // / återbetala } }
private void SendReceipt() { /* ... */ } private void SendTrackingLink() { /* ... */ } private void UpdateTracking() { /* ... */ } private void Refund() { /* ... */ }}Varje pil i diagrammet blir en rad i switch-uttrycket. when cardApproved är villkoret — och raden under bestämmer vad som händer när villkoret inte är sant: ordern står kvar i Created. Och den sista raden, _ => throw, är alla pilar som inte finns — försöker någon avbryta en skickad order får de ett tydligt fel istället för en trasig order.
Nästa steg: Växer tillståndsmaskinen, så att varje tillstånd får mycket eget beteende, är designmönstret State nästa steg: en klass per tillstånd istället för en stor switch.
När ska du välja ett tillståndsdiagram?
| Välj tillståndsdiagram när… | Välj något annat när… |
|---|---|
| Ett objekt har en livscykel med tydliga lägen | Du beskriver ett arbetsflöde med flera roller → aktivitetsdiagram |
| Samma händelse ska ge olika resultat beroende på läge | Det viktiga är ordningen på anropen mellan objekt → sekvensdiagram |
| Du behöver bestämma vilka övergångar som är förbjudna | Du vill visa vilka klasser som finns och hur de hänger ihop → klassdiagram |
Du har en enum Status och många if (status == ...) | Det är en algoritm som räknar ut något i steg → flödesschema |
Vanliga misstag
- Handlingar som tillstånd. “Skicka paket” är något som görs — det hör hemma på en pil eller i ett aktivitetsdiagram. Tillståndet heter
Skickad. - Glömt initialtillstånd. Utan den fyllda pricken vet ingen var objektet börjar.
- Pilar utan händelse. Om det inte står vad som orsakar övergången går den inte att koda.
- Villkor som överlappar.
[belopp ≥ 20]på en pil och[belopp > 10]på en annan, från samma tillstånd och för samma händelse — båda kan vara sanna samtidigt. - Bara den lyckliga vägen. Fråga för varje tillstånd: vad händer om användaren avbryter här? Om betalningen misslyckas?
Övning
Rita ett tillståndsdiagram för ett supportärende: Ny, Tilldelad, Pågår, Väntar på kund, Löst och Stängd. Bestäm själv vilka händelser som flyttar ärendet, och använd minst ett villkor (t.ex. stäng [kunden har svarat]) och en självövergång (t.ex. ny kommentar / meddela handläggare). Implementera sedan övergångarna i C# med en enum och ett switch-uttryck.
TL;DR
| Begrepp | Vad det är |
|---|---|
| Tillståndsdiagram | Vilka lägen något kan vara i, och vad som byter läge |
| Rundad ruta | Ett tillstånd — något objektet är |
| Pil | En tillåten övergång. Ingen pil = förbjudet |
händelse [villkor] / handling | Vad som triggar, när det är tillåtet, vad som görs |
| I C# | enum för tillstånden + switch-uttryck på (tillstånd, händelse) |