Jag har egentligen inte i mitt tidigare liv stött på konceptet tillståndsmaskin när det handlar om programmering, även om jag har ett svagt minne av att begreppet dök upp i något av de tekniska ämnen jag läste på gymnasiet. Det var inte alls särskilt länge sedan ämnet dök upp i en programmeringsteknisk diskussion med sambon. Han, som har både mer formell utbildning och bättre teoretiska kunskaper än jag, förklarade principen och jag blev entusiastisk och lanserade konceptet på jobbet och fick positiv respons och sedan har det rullat på. Jag gillar tillståndsmaskiner!
Jag tror inte att jag är världsbäst på att förklara det hela, men kan ju ändå försöka ge min bild. Konceptet innebär att saker hela tiden befinner sig i ett visst tillstånd, och går vidare till andra tillstånd enligt ett förbestämt mönster eller schema – inte nödvändigtvis likadant från gång till gång. Ett eller flera tillstånd kan t ex upprepas eller måste inte finnas alls. Alla sådana saker specificeras av mönstret/schemat.
Förresten, det är ju inte så att det alltid finns en formell beskrivning heller – ibland sitter det hela bara i huvudet, man förstår själva processen. Det kan vara så enkelt som att man köper en sak: går in i affären, väljer vara (eller varor), betalar, får ett kvitto, går ut ur affären. Eller så låter man bli att betala och får därför inget kvitto. Eller kanske man går runt i butiken varv efter varv innan man till slut känner sig redo att passera kassan. Och så vidare. Processen är flexibel och kan varieras.
En fördel med att använda tillståndsmaskiner i programmeringen är att man slipper hålla hela bilden i huvudet. Det räcker med att se på den närmaste omgivningen och förstå var man man kommer ifrån, och vad man därför ska göra eller förvänta sig härnäst. Typ så ser jag på det. En tillståndsmaskin gör jobbet mycket enklare. Och ja, för att det påståendet ska vara sant måste det självfallet handla om ett problem som är lämpligt att lösa med en tillståndsmaskin, men det säger sig självt.
Utan tillståndsmaskinen kommer man antagligen att skriva kod som är kladdig och svårläst och därmed blir knepig både att följa, underhålla och vidareutveckla. Och kladdig och svårbegriplig kod tycker jag är bland det värsta en programmerare kan skriva. Det är att göra alla, både sig själv och framtida förvaltare av koden, en mycket stor otjänst. Om koden är kladdig från början blir den inte bättre när man ändrar i den. Inte ens om man själv är den som skrivit den från början. Tro mig.
Här skulle jag lätt kunna dra ut på sidospår för att prata programmeringsmetodik i största allmänhet (Code Complete-läsandet sätter sina spår och väcker sovande björnar), men nu tror jag ändå att det var historien om tillståndsmaskinen jag hade tänkt berätta så jag låter bli det. Kanske någon annan gång. Till programmeringen!
Låt säga att vi ska jobba oss igenom någon form av datastruktur, som är/behöver vara ordnad efter ett visst schema, och behöver titta på eller hantera varje bit data på olika sätt beroende på var i det här schemat vi (d.v.s. vi som lever i koden) befinner oss. Då tuggar vi oss igenom strukturen bit för bit och gör det som behövs för var och en av dem. För att hålla reda på var i schemat vi befinner oss kan vi t ex använda states, eller tillstånd om man nu ska prata svenska. Fine. Det finns ju i Java (och även andra språk) en datatyp för uppräkning och den kan vi använda.
Vi har en variabel som representerar det aktuella tillståndet med hjälp av ett värde ur en enumeration (uppräkning, här: lista över möjliga tillstånd), och så gör vi saker och sätter om värdet på variabeln vartefter vi tar oss fram i schemat/mönstret. Och på slutet kollar vi om det slutliga tillståndet stämmer med vad vi förväntat oss. Allt är fint, men koden känns inte riktigt bra och framförallt när man hanterar många olika tillstånd blir det väldigt oöverskådligt. Man tycker att det borde kunna finnas bättre sätt att lösa problemet. Och det gör det förstås. Det gör det nästan alltid.
Jag och sambon pratade vidare om tillståndsmaskiner och han planterade för några dagar sedan en tanke om att i stället representera tillstånden med klasser, som då skulle ha en hanteringsmetod med den logik som annars ligger i case-satserna. Det lät roligt, om än lite svårt att föreställa sig först, så jag blev sugen på att åtminstone testa. Sent igår gjorde jag sålunda ett provskott och kunde då konstatera att det var snabbt och lätt att skapa tillståndsklasser och peta in hanteringsmetoderna i dem.
Därefter åkte jag hem och fortsatte där med att skapa fiktiv exempelkod, helt tagen ur luften, och visa för sambon hur hans lösning förvanskats och implementerats av min hjärna och nu tänkte jag att jag kunde visa lösningen även för mitt framtida jag. Och naturligtvis även för er som läser detta inlägg och eventuellt är intresserade.
Så här ser det ut när jag representerar mina tillstånd med hjälp av enumerationer:
/* metoden som gör det övergripande jobbet */ {
State currentState = Begin;
while (we.haveMoreThings()) {
switch (currentState) {
case Begin:
currentState = we.handleBegin(thing);
break;
case StateA:
currentState = we.handleStateA(thing);
break;
case StateB:
currentState = we.handleStateB(thing);
break;
case StateC:
currentState = we.handleStateC(thing);
break;
case End:
currentState = we.handleEnd(thing);
break;
default:
throw new IllegalStateException("Hey, what!?");
}
}
if (!currentState.isTheEnd()) {
throw new IllegalStateException("We are not done yet!");
}
}
private State handleBegin(Object thing) {
// Här gör vi massor av spännande hantering...
// ... och sedan returnerar vi nytt tillstånd:
return StateA;
}
// ... här blir det alltså en metod per hanterat tillstånd.
Och så här ser det ut när jag i stället representerar mina tillstånd med klasser:
/* metoden som gör det övergripande jobbet */ {
State currentState = Begin;
while (we.haveMoreThings()) {
currentState = currentState.handle(thing);
}
if (!currentState.isTheEnd()) {
throw new IllegalStateException("We are not done yet!");
}
}
Det vill säga: tillståndet sköter hanteringen. Jag vet vilken version jag föredrar.
Så, kan man kanske undra, hur ser det då ut bakom kulisserna?
I det första fallet lade jag (av flera olika skäl) mina tillstånd i egen, separat fil:
enum State {
Begin, StateA, StateB, StateC, End;
public boolean isTheEnd() {
return this == End;
}
}
… och i det andra fallet hamnar handle-metoderna i stället i ett State-interface, där jag även lagt tillståndsklasserna (som varken behövs eller bör synas utifrån). Det är en bra idé att stoppa undan ovidkommande information för att undvika förvirring.
interface State {
public State handle(Object thing);
public boolean isTheEnd();
public static final State Begin = new Begin();
public static final State StateA = new StateA();
public static final State StateB = new StateB();
public static final State StateC = new StateC();
public static final State End = new End();
public class Begin implements State {
@Override
public State handle(Object thing) {
// Här gör vi massor av spännande hantering...
// ... och sedan returnerar vi nytt tillstånd:
return StateA;
}
@Override
public boolean isTheEnd() {
return false;
}
private Begin() { /* no outside construction, please */ }
}
// ... här blir det alltså en klass per hanterat tillstånd.
}
Hade detta varit i den riktiga världen hade jag valt att lägga tillståndsklasserna i egna, separata filer och gjort dem ”package private”, men i exempelkoden ville jag hålla strukturen likadan utifrån sett för att göra jämförelsen dem emellan enklare.
Nåväl, så här ser det ut just nu. Exemplet ovan gör inga anspråk på att vara vare sig korrekt eller komplett eller ens i närheten av realistiskt, men jag tycker att det fyller en funktion och hoppas att någon av mina läsare får glädje eller nytta av det.
Själv kommer jag garanterat att fundera vidare och skruva och fixa och dona och fiffa och mojja och vad man nu gör med kod. Kod, och ens tänkande kring den, evolverar ständigt. Ibland går det snabbt och ibland går det långsamt, men jag finner det i praktiken omöjligt att stå stilla. Det går alltid att göra koden bättre!
Kommentarer, funderingar, förslag?
Tryckt & kränkt