Liigu peamise sisu juurde

State

Sissejuhatus

Paljud objektid käituvad erinevalt sõltuvalt oma olekust. Selle tagamiseks kõige lihtsam viis on hoida antud olek mingi väljana klassis ning loogikat teostada if/else-lauseste abil. Selline lähenemine muutub kiiresti kohmakaks: iga uus olek lisab harusid mitmesse meetodisse, iga meetod dubleerib samu kontrolle ning klass muutub tingimuste sasipuntraks.

Olekumuster (state) eraldab iga oleku omaette klassi. Kontekstiobjekt hoiab endas viidet hetkeolekule ja delegeerib olekuspetsiifilise käitumise sellele. Kui olek muutub, asendatakse viide teise objektiga. Väljast vaadates näib konteksti käitumine muutuvat, sest see delegeeritakse nüüd teisele klassile.

Probleemist lähemalt

Toome näiteks müügiautomaadi. Lihtsustatud mudelis on sellel kolm olekut:

  • Idle - ootab raha, toote valikut pole tehtud.
  • HasMoney - raha on sisestatud, ootab toote valikut.
  • Dispensing - toode on valitud ja seda väljastatakse.

Iga olek käsitleb samu kolme tegevust (insertMoney, selectProduct, dispense) erinevalt. Ilma olekumustrita väljendub see ühe olekulipu ja harunevate meetoditena:

public class VendingMachine {
private enum MachineState { IDLE, HAS_MONEY, DISPENSING }
private MachineState state = MachineState.IDLE;

public void insertMoney(int amount) {
if (state == MachineState.IDLE) {
System.out.println("Money accepted.");
state = MachineState.HAS_MONEY;
} else if (state == MachineState.HAS_MONEY) {
System.out.println("More money added.");
} else if (state == MachineState.DISPENSING) {
System.out.println("Please wait, dispensing in progress.");
}
}

public void selectProduct(String product) {
if (state == MachineState.IDLE) {
System.out.println("Please insert money first.");
} else if (state == MachineState.HAS_MONEY) {
System.out.println("Dispensing " + product + "...");
state = MachineState.DISPENSING;
} else if (state == MachineState.DISPENSING) {
System.out.println("Already dispensing.");
}
}

// ... and so on for every action
}

OutOfStock oleku lisamine tähendab uue haru lisamist igasse meetodisse. Klassis on juba kolm meetodit, igaühes kolm haru. See tähendab ühe uue oleku jaoks üheksat muudatuskohta. Iga lisandusega muutub olukord veelgi keerulisemaks.

Olekumuster

Antud muster toob sisse liidese või abstrakse klassi, mis esindab olekust sõltuvaid käitumisi, ning iga oleku jaoks eraldi konkreetse klassi, mis seda abstraktsiooni teostab:

public abstract class VendingMachineState {
protected VendingMachine machine;

public VendingMachineState(VendingMachine machine) {
this.machine = machine;
}

public void insertMoney(int amount) {
throw new IllegalStateException("Cannot insert money in state: " + getStateName());
}

public void selectProduct(String product) {
throw new IllegalStateException("Cannot select product in state: " + getStateName());
}

public void dispense() {
throw new IllegalStateException("Cannot dispense in state: " + getStateName());
}

public abstract String getStateName();
}

Iga olek teostab ainult neid meetodeid, mida see toetama peaks:

public class IdleState extends VendingMachineState {

public IdleState(VendingMachine machine) {
super(machine);
}

@Override
public void insertMoney(int amount) {
System.out.println("Money accepted: " + amount);
machine.setState(new HasMoneyState(machine, amount));
}

@Override
public String getStateName() {
return "Idle";
}
}
public class HasMoneyState extends VendingMachineState {
private int balance;

public HasMoneyState(VendingMachine machine, int balance) {
super(machine);
this.balance = balance;
}

@Override
public void insertMoney(int amount) {
balance += amount;
System.out.println("Additional money accepted. Balance: " + balance);
}

@Override
public void selectProduct(String product) {
System.out.println("Selected: " + product + ". Dispensing...");
machine.setState(new DispensingState(machine, product));
}

@Override
public String getStateName() {
return "HasMoney";
}
}
public class DispensingState extends VendingMachineState {
private String product;

public DispensingState(VendingMachine machine, String product) {
super(machine);
this.product = product;
}

@Override
public void dispense() {
System.out.println("Here is your " + product + ". Enjoy!");
machine.setState(new IdleState(machine));
}

@Override
public String getStateName() {
return "Dispensing";
}
}

Kontekstiklass hoiab viidet hetkeolekule ja delegeerib sellele kõik olekust sõltuvad tegevused:

public class VendingMachine {
private VendingMachineState state;

public VendingMachine() {
this.state = new IdleState(this);
}

public void setState(VendingMachineState state) {
this.state = state;
}

public void insertMoney(int amount) {
state.insertMoney(amount);
}

public void selectProduct(String product) {
state.selectProduct(product);
}

public void dispense() {
state.dispense();
}
}

Selle kasutamine näeks välja selline:

VendingMachine machine = new VendingMachine();

machine.insertMoney(150); // "Money accepted: 150"
machine.selectProduct("Cola"); // "Selected: Cola. Dispensing..."
machine.dispense(); // "Here is your Cola. Enjoy!"
machine.insertMoney(100); // "Money accepted: 100" (back to idle → hasMoney)

Kui proovitakse teha tegevust, mida praegune olek ei toeta, siis see lõppeb veateatega:

machine.selectProduct("Cola");  // throws IllegalStateException: "Cannot select product in state: Idle"

Piltlikult näeks olekute muutmine selline välja:

Pane tähele:

  • Iga olekuklass on väike ja keskendub ühele asjale: mida see olek lubab. Puuduvad olekuteülesed if-harud.
  • Uue oleku lisamine tähendab ühe uue klassi kirjutamist. Teised olekuklassid ega VendingMachine ise ei muutu.
  • Olekutevahelisi üleminekuid haldavad olekuklassid ise. IdleState teab, et raha sisestamine viib HasMoneyState-i. See teadmine on lokaliseeritud, mitte laiali paisatud ühte kesksesse klassi.
  • Vigased üleminekud püütakse kinni baasklassi vaikimisi teostuses. Kontekstis ei ole vaja eraldi käsitlust.

State vs Strategy

Oleku- ja strateegiamuster näevad struktuurilt sarnased välja - mõlemal juhul hoiab kontekst viidet liidesele ja delegeerib tegevusi sellele. Erinevus seisneb eesmärgis ja selles, kes viidet muudab.

Strateegiamustri puhul valib algoritmi kutsuja ja määrab selle selgesõnaliselt. Kontekst ei muuda oma strateegiat ise.

Olekumustri puhul käivitavad olekud ise üleminekud, kutsudes setState(...) meetodit. Kontekst muutub automaatselt omaenda käitumise tulemusena.

Lühidalt:

  • Strateegiamuster käsitleb kuidas midagi teha.
  • Olekumuster käsitleb seda, kuidas objekt muutub ajas millekski teiseks.

Kasutusjuhud

Olekumuster on sobilik, kui:

  • Objekti käitumine sõltub selle sisemisest olekust ja peab käigu pealt muutuma.
  • Meetodid sisaldavad suuri tingimusblokke, mis lülituvad sama olekumuutuja alusel.
  • Olekutevahelised üleminekud järgivad kindlaid reegleid ning soovid, et need oleksid rangelt tagatud, mitte lihtsalt eeldatud.
  • Tahad lisada uusi olekuid ilma olemasolevat olekuloogikat muutmata.

See on ebavajalik objektide puhul, millel on ainult üks või kaks käitumist ja mida tõenäoliselt rohkem juurde ei lisandu. Kui olekust sõltuv loogika on minimaalne, on väli ja mõned tingimuslaused sageli selgem lahendus.