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 enum och 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 enum och en switch

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.

Tillståndsdiagram för en kaffeautomat med namngivna delar: initialtillstånd, tillstånd, självövergång, övergång med händelse, villkor och effekt, sammansatt tillstånd med deltillstånd, samt sluttillstånd Initialtillstånd Redo mynt [belopp < 20] stäng av mynt [belopp ≥ 20] / starta klar / pip Brygger Maler malt Värmer Självövergång Sluttillstånd Tillstånd Händelse (trigger) Villkor [guard] Effekt (/ handling) Övergång Sammansatt tillstånd Deltillstånd
DelHur den ritasVad den betyder
Initialtillstånd (initial state)Fylld prick med en pilVar objektet börjar. Det är inget riktigt tillstånd — bara en pekare till det första
Tillstånd (state)Ruta med rundade hörnEtt 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 annatObjektet byter läge. Allt som inte har en pil är förbjudet
Händelse (event, trigger)Första ordet vid pilenDet 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å radenNå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åndHändelsen hanteras, effekten utförs — men tillståndet blir detsamma
Sammansatt tillstånd (composite state)Stor rundad ruta med egna tillstånd inutiEtt 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.

Tillståndsdiagram för en beställning: Skapad, Betald, Skickad och Levererad i en kedja, med Avbruten som nås från Skapad och Betald, och en självövergång på Skickad Skapad betala [kortet godkänt] / skicka kvitto Betald skicka / mejla spårningslänk Skickad ny position / uppdatera spårning kunden kvitterar Levererad avbryt avbryt / återbetala Avbruten

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ägenDu beskriver ett arbetsflöde med flera roller → aktivitetsdiagram
Samma händelse ska ge olika resultat beroende på lägeDet viktiga är ordningen på anropen mellan objekt → sekvensdiagram
Du behöver bestämma vilka övergångar som är förbjudnaDu 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

BegreppVad det är
TillståndsdiagramVilka lägen något kan vara i, och vad som byter läge
Rundad rutaEtt tillstånd — något objektet är
PilEn tillåten övergång. Ingen pil = förbjudet
händelse [villkor] / handlingVad 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)