JavaSvet - otvorena java zajednica

 
glavna stranica arr2javasvet  english version arr2java.net

State patern u borbi protiv threadova

Predrag Spasojević
24 Jan 2005
Sadržaj:

Ovaj članak je nastao na osnovu iskustva na razvoju jednog "multi-thread" sistema u čijoj su osnovu nekoliko komponenti baziranih na state paternu. Velika zasluga za ovaj članak pripada i ljudima koji su pored mene učestvovali na tom projektu a to su Dušan Petrović i Perica Milošević.

Sleng

Članak je napisan korišćenjem "srpsko-engleskog" programerskog slenga. Dakle nisu prevođeni stručni termini koji nemaju opšte prihvaćen prevod na sprski jezik.Oni su napisani u originalnon engleskom obliku ali su zato na njih primenjena sva gramatička pravila srpskog jezika npr menjanje kroz padeže i sl.

Motivacija

Ovaj članak će prikazati različite aspekte i implementacije sistema sa stanjem korišćenjem state paterna. Sistemi sa stanjem se po pravilu koriste u multi-thread okruženju tako da će fokus članka biti na odnosu state paterna i threadova. Na kraju članka će na osnovu zaključaka analize state paterna biti započet razvoj univerzalne komponente za rukovanje promenama stanja koja se može primeniti da bilo koji objekat sa stanjem.

Osnove State paterna

UML Diagram



Učesnici

Opis

Ideja state paterna je da se upravljanje promenama stanja izmesti van Konteksta. Za svako konkretno stanje Konteksta se pravi posebna klasa koja rukuje promenama stanja koje predstavlja.

Prednosti: Mane:

State patern na delu

State patern će biti prikazan kroz jedan jednostavan primer. Treba napraviti klasu koja reprezentuje robota čistačicu. On može da se upali, ugasi i može da mu se naredi da čisti.

Datim robotom može više osoba paralelno da upravlja. Treba da obezbediti konzistentnost prelaska stanja u takvom okruženju.

Prikaze implementacije state paterna počećemo od najjednostavnije varijante. Zbog veličine prostora biće prikazana implementacija Konteksta, Stanja i samo jednog konkretnog stanja. Implementacija ostalih konkretnih stanja je veoma slična.

Kontekst


public class RobotCistacica {
 
private CistacicaState stanje;

 
public RobotCistacica() {
  }

 
public synchronized void upaliSe()
  {
// uradi nesto
   
stanje.upaliSe();
 
}
 
public synchronized void ugasiSe()
  {
//uradi nesto
   
stanje.ugasiSe();
 
}
 
public synchronized void pocisti()
  {
//uradi nesto
   
stanje.pocisti();
 
}
 
public CistacicaState getStanje() {
   
return stanje;
 
}
 
public void setStanje(CistacicaState stanje) {
   
this.stanje = stanje;
 
}
}

Stanje

public abstract class CistacicaState {

 
private RobotCistacica cistacica = null;

 
public CistacicaState(RobotCistacica cistacica) {
   
this.cistacica = cistacica;
 
}

 
public void upaliSe() {

  }

 
public void ugasiSe() {

  }

 
public void pocisti() {

  }

 
public RobotCistacica getCistacica() {
   
return cistacica;
 
}

}

KonkretnoStanje

public class StanjeUpaljen extends CistacicaState {

 
public StanjeUpaljen(RobotCistacica cistacica) {
   
super(cistacica);
 
}
 
public void ugasiSe()
  {
   
//uradi nesto
   
StanjeUgasen ugasen= new StanjeUgasen(getCistacica());
    getCistacica
().setStanje(ugasen);
 
}

 
public void pocisti()
  {
   
StanjeCisti cisti= new StanjeCisti(getCistacica());
    getCistacica
().setStanje(cisti);
 
}

}

Primetićete da su sve metode promene stanja konteksta synchronized čime se obezbeđuje "thread safe" promena stanja.

Kako implementirati Stanje

Stanje u gore navedenom primeru je implementirano kao apstraktna klasa. Druga mogućnost je da to bude interface ali takvo rešenje ne volim. Dobro je da sve metode promene stanja imaju svoje podrazumevano ponašanje. Osim toga, implementacija Stanja kao apstraktne klase omogućava da Konkretna Stanja implementiraju samo one metode koje to stanje prevode u drugo što ne bi bio slučaj kada bi Stanje bilo interface.

Ko menja stanje

U gore navedenom primeru Konkretno Stanje je zaduženo za promenu stanja Konteksta. U tom slučaju svako Konkretno Stanje mora imati referencu na Kontekst i setState() metoda Konteksta mora biti protected ili public. Prednost ovog rešenja je u tome što je kompletna logika prelaska stanja enkapsulirana u Konkretnom Stanju. Negde je i logično da Konkretno Stanje zna za datu akciju u koje stanje treba sistem da pređe. Mana ovog rešenja je što setState metoda konteksta mora biti dostupna široj javnosti. Time je omogućeno da njenim direktnim pozivanjem Kontekst dođe u nekonzistentno stanje.

Druga moguća implementacija je da sam Kontekst menja svoje stanje. U tom slučaju od Konkretnog Stanja mora dobiti informaciju u koje stanje da pređe.

Kontekst

public class RobotCistacica {
 
private CistacicaState stanje;

....
....
 
public synchronized void upaliSe()
  {
   
CistacicaState novoStanje = stanje.upaliSe();
    setStanje
(novoStanje).
 
}
...
...
}

Ovakvom implementacijom je sačuvana privatnost setState metode ali je logika promene stanja razbijena u dve klase. KonkretnoStanje izvršava sve radnje koje nastaju prilikom promene stanje a Kontekst menja stanje. Mislim da nije dobro da klasa radi ono što nije njen posao. U ovom slučaju ne sviđa mi se što Kontekst mora da vodi računa da pravilno promeni svoje stanje. Kao najbolje rešenje čini mi se uvođenje StateManager klase. Njeno zaduženje bi bilo sve što je vezano za stanje objekta. Kontekst bi imao referencu na StateManagera a on bi imao referencu na stanje. Implementacija bi izgledala od prilike ovako:

Kontekst

public class RobotCistacica {
 
private CistacicaStateManager manager;

 
public RobotCistacica() {
  }

 
public void upaliSe()
  {
   
manager.upaliSe();
 
}

StateManager

public class CistacicaStateManager {
 
private CistacicaState stanje;

....
....
 
public synchronized void upaliSe()
  {
   
CistacicaState novoStanje = stanje.upaliSe();
    setStanje
(novoStanje).
 
}
...
...
}

Time smo sačuvali privatnost setState metode a i Kontekst ne upravlja promenana stanja. Mana ovog pristupa je još jedna klasa koja u ovom trenutku može delovati kao višak. Zbog jednog poziva setState() kreirana je nova klasa. Međutim čest je slučaj da sistem poseduje još funkcija vezanih za promenu stanja. Npr okidanje događaja promene stanja i obaveštavanja svih slušalaca o tome. Dobro je da se kontekst što više rastereti brige o upravljanju stanjima.

Instanciranje Konkretnih stanja

Podsetimo se kako izgleda implementacije metode promene stanja u konkretnom stanju:

  public void ugasiSe()
  {
   
// uradi nesto
   
StanjeUgasen ugasen= new StanjeUgasen(getCistacica());
    getCistacica
().setStanje(ugasen);
 
}

U ovom primeru kada god se menja stanje kreira se novi objekat Konkretnog Stanja. Ovo je dobar pristup samo kada su promene stanja retke. Kada su promene česte konkretna stanja se stalno kreiraju i uništavaju što je dodatni nepotrebni posao. Po pravilu Konkretna Stanja nemaju svoje stanje u smislu da nemaju svoje atribute. Dovoljno je da se za svaki objekat Konteksta kreira po jedna instanca svakog stanja. Ovo rešenje nije dobro kada objekat ima mnogo stanja. U tom slučaju bi se za vreme životnog veka Konteksta u memoriji čuvala sva njegova potencijalna stanja.
U odgovarajućim implementacijama Konkretna Stanja mogu biti singletoni. Kako konkretna stanja obično manipulišu sa Kontekstom da bi ona bila singletoni metode promene stanja moraju imati Kontekst objekat kao atribut.

  public void ugasiSe(RobotCistacica kontekst)
  {
   
// uradi nesto
       
kontekst.setStanje(StanjeUgasen.getInstance());
 
}

Šta raditi dok je Kontekst u fazi promene stanja

Kod sistema sa stanjem primetio sam četiri različita ponašanja u slučajevima kada se zatraži promena stanja dok je sistem u fazi promene stanja. Sistem je u fazi promene stanja dok je aktivna neka od metoda promene stanja.

  1. Zahtev se stavlja u red čekanja i izvršiće se nakon što se završi promena stanja.
  2. Ako zahtev ne menja postojeće stanje odmah će odgovoriti klijentu da ništa nije promenjeno. Ako menja stanje stavlja se u red čekanja.
  3. Odmah će se odgovoriti klijentu da je nije bilo promene stanja. Odnosno zabranjeno je davati zahteve za promenu stanja dok je sistem u fazi promene stanja.
  4. Poseban slučaj je kada klijent od trenutka kada dobije informaciju u kom je Kontekst stanju pa dok ne odluči da li izvrši neku akciju ne želi da se stanje može promeniti. U ovom slučaju faza promene stanja počinje od trenutka dobijanja informacije o trenutnom stanju.

Svi zahtevi se stavljaju u red čekanja

Ovo je najčešći slučaj i najjednostavniji je za implementaciju. Ovo je slučaj koji smo implementirali u našem prvom primeru. Dovoljno je da sve metode promene stanja konteksta budu synchronized.

Zahtevi koji ne menjaju stanje se odmah izvršavaju, ostali u red čekanja

Često je nepotrebno da zahtevi koji neće izazvati promenu postojećeg stanja čekaju da se trenutni proces promene stanja završi. Ovde ima jedna začkoljica, šta ako bi ti zahtevi izazvali promenu stanja sistema u kome će biti nakon procesa promene stanja. Npr čistačica je u stanju ugašen i trenutno je aktivna metoda upaliSe. Od klijenta stiže zahtev da se ugasi. Pošto je sistem u stanju ugašen taj zahtev neće promeniti postojeće stanje ali će promeniti stanje u koje će sistem doći nakon završetak metode upaliSe. Da li treba odmah izvršiti zahtev ili ga staviti u red čekanja? Odgovor na ovo pitanje zavisi od potreba konkretnog sistema. Dok je aktivna promena stanja mi nikako ne možemo znati u koje će stanje ona prebaciti sistem i samim tim ne možemo znati da li će novi zahtev promeniti ili neće buduće stanje. Ako hoćemo da takve zahteve stavimo u red čekanja onda moramo sve zahteve da stavimo u red čekanja i time dolazimo do prethodno opisanog slučaja.

Klijenti često na osnovu tekućeg stanja odlučuju koju akciju nad sistemom da izvrše tako da su zahtevi vezani za trenutno a ne buduće stanje. U većini slučajeva će postupak da se odmah izvrše metode koje ne menjaju postojeće stanje biti korektan. Kada je čistačica ugašena i klijent traži iz nekog razloga da se ona ugasi potpuno je korektno da mu se odmah odgovori i čistačica će zaista biti ugašena onog trenutka kada klijent dobije odgovor. Mislim da nema smisla da se čeka da se čistačica upali da bi se ponovo ugasila.

Jedna moguća implementacija ovoga bi bila da se Stanje implementira kao abstraktna klasa sa ne sinhronizovanim metodama. Konkretna Stanja treba da implementiraju samo one metode koje će promeniti stanje i te metode treba da imaju synchronized block.

Stanje

public abstract class CistacicaState {

 
private RobotCistacica cistacica = null;

 
public CistacicaState(RobotCistacica cistacica) {
   
this.cistacica = cistacica;
 
}

 
public void upaliSe() {

  }

 
public void ugasiSe() {

  }

 
public void pocisti() {

  }

 
public RobotCistacica getCistacica() {
   
return cistacica;
 
}

}

KonkretnoStanje

public class StanjeUpaljen extends CistacicaState {

 
public StanjeUpaljen(RobotCistacica cistacica) {
   
super(cistacica);
 
}
 
public void ugasiSe()
  {
   
synchronized(getCistacica())
{
// ova provera mora da se uvede jer u ovom trenutku cistacica mozda nije u ovom stanju.
    
if (getCistacica().getStanje()!=this)
    {
   
return getCistacica().getStanje().ugasiSe();
   
}
   
// uradi nesto
   
StanjeUgasen ugasen= new StanjeUgasen(getCistacica());
    getCistacica
().setStanje(ugasen);
}
}
....
}

Kako se sinhronizacija vrši u konkretnim stanjima, a ne u kontekstu kao u prethodnom slučaju, svaka metoda promene stanja Konkretnog Stanja mora proveriti da li je sistem još uvek u tom stanju. Ako nije treba proslediti zahtev trenutnom stanju.

Drugo rešenje bi bilo korišćenjem refleksije gde bi se proverilo da li postoji u KonkretnomStanju data metoda. Time bi se izbegla potreba da KonkretnoStanje proverava da li je Kontekst još uvek u tom stanju.

Dok je promena stanja aktivna svi novi zahtevi se odbijaju

Ima sistema kod kojih je ovakav princip potreban. Opet polazimo od činjenice da klijent na osnovu postojećeg stanja donosi odluku o akcijama nad Kontekstom. Ako u toku prelaska stanja dođu novi zahtevi da se menja stanje oni su obično posledica informacije o postojećem stanju i želji da se ono promeni. Da je klijent znao da će se ta akcija izvršiti nad nekim drugim stanjem možda bi drugačiju odluku doneo.

Implementacija zahteva uvođenje flaga da li je aktivna promena stanja ili nije. I dok je aktivna da se svi drugi zahtev odbijaju. Najveći problem je napraviti "thread safe " manipulaciju flagom.

Kontekst

public class RobotCistacica {
 
private CistacicaState stanje;
 
private Boolean canChangeState = Boolean.TRUE;

 
public RobotCistacica() {
  }
....
....
 
public void upaliSe()
  {
   
if (startStateChange())
    {
     
try
     
{
   
stanje.upaliSe();
     
}
     
finally
     
{
       
stopChangeState();
     
}
    }
  }
 
private boolean startStateChange()
  {
   
synchronized( canChangeState)
    {
     
if (canChangeState == Boolean.TRUE)
      {
       
canChangeState = Boolean.FALSE;
   
return true;
     
}
     
return false;
   
}

  }
 
private void stopChangeState()
  {
   
synchronized(canChangeState)
    {
       
canChangeState = Boolean.TRUE;
   
}

  }
}

Obratiti pažnju na to da se stopChangeState() se obavezno mora pozivati iz finally bloka kako bi se obezbedilo da ako se bilo sta desi prilikom promene stanja ipak bude pozvana ta metoda.

Bitno: Ako metoda promene stanja dugo traje treba razmišljati u pravcu uvođenja novog stanja. Npr ako bi se naša čistačica dugo startovala trebalo bi uvesti stanje Startovanje. Time bi dobili brz prelazak iz stanja u stanje a dok se startuje mogle bi se izvršiti neke od metoda promene stanja npr mogla bi se ugasiti.

Proces promene stanja počinje od trenutka poziva getStanje() metode

Čest je slučaj da klijent od trenutka dobijanja informacije o trenutnom stanju pa sve dok ne donese odluku da li da promeni stanje ili ne, ne želi da bilo ko drugi menja stanje Konteksta. Ovaj slučaj pomera granicu početka procesa promene stanje. U prethodnim primerima promena stanja je počinjala od trenutka poziva metode promene stanja a u ovom slučaju počinje od trenutka dobijanja informacije o stanju.

Implementacija bi bila slična trećem slučaju sa tim što bi startChangeState i stopChangeState bile javne metode i ne bi se pozivale iz konteksta. Najpre bi klijent pozvao startChangeState, onda saznao tekuće stanje, pozvao metodu promene stanja i na kraju pozvao stopChangeState().

Za ovako definisan trenutak početka promene stanja bi se mogli implementirati sva tri načina ponašanja sistema dok je u fazi promene stanja. No to prevazilazi obim ovog članka pa neće ovde biti prikazani.

Šta kada sistem dođe u određeno stanje

Dolazak u određeno stanje po pravilu inicira niz radnji nad Kontekstom. Sa druge strane iz metode promene stanja treba izaći čim sistem promeni stanje. To dovodi do toga da metoda promene stanja ne sme direktno pozvati metodu koja se aktivira ulaskom u određeno stanje nego to mora uraditi u posebnom threadu. Dobro rešenje je korišćenje Observer paterna. Nevezano za ovo, čest je slučaj da mnogi žele biti obavešteni kada sistem uđe ili napusti određeno stanje.

Komponenta za upravljanje promenama stanja

Kao rezultat analize raznih apsekata borbe sa stanjima i uz jednu žlicu refleksije napravićemo univerzalnu komponentu za rukovanje promenama stanja koja se može primeniti na bilo koji Kontekst. Komponenta će imati sledeće karakteristike:

Implementacija StateManagera kao i odgovarajuća implementacija RobotaCistacice priložena je uz članak. Ovde cemo samo dati kratak opis nekih metoda komponente

StateManager

public StateManager(AbstractState initialState, int mode)

Konstrukotrom se postavlja pocetno stanje Konteksta kao i mod rada StateManagera.

public HashMap changeState(String methodName) throws Exception

Da bi smo napravili univerzalnu komponentu morala je da postoji samo jedna jedina metoda koja menja stanje. Njoj se kao parametrar prosledjuje naziv metode KonkretnogStanja koju treba da izvrši. Pomoću refleksije ona poziva datu metodu. Zavisno od moda rada metode changeState će se različito ponašati.

Stanje

public abstract class AbstractState {

 
private Object context = null;

 
public AbstractState(Object context) {
   
this.context = context;
 
}

 
public Object getContext() {
   
return context;
 
}

}

Naš primer bi korišćenjem StateManager-a izgledao ovako:

Kontekst

import net.javasvet.*;
public class RobotCistacica {
 
private StateManager stateManager;
 
public RobotCistacica() {

   
StanjeUgasen ugasen = new StanjeUgasen(this);
    stateManager =
new StateManager(ugasen,StateManager.MODE_SYNC);
 
}

 
public void upaliSe() throws Exception{
   
stateManager.changeState("upaliSe");
 
}
...
...
}

KonkretnoStanje

import net.javasvet.*;

public class StanjeUgasen
   
extends AbstractState {

 
public StanjeUgasen(Object context) {
   
super(context);
 
}

 
public  StateResponse upaliSe() throws Exception{
     
// uradi nesto
     
StanjeUpaljen upaljen = new StanjeUpaljen(getContext());
      StateResponse response=
new StateResponse(upaljen, null);
     
return response;
}
}

Pravci razvoja StateManagera

Događaji. Prilikom ulaska u novo stanje može se okinuti Event i može se implementirati i njemu odgovarajući Listener. Eventi se mogu okinuti i pre i posle izvršavanja metode promene stanja, po ulasku i po izlasku iz stanja...

Parametri metode promene stanja. Trenutno metode promene stanja nemaju paramtera. Nekada je potrebno da ih imaju pa ne bi bilo loše da im se doda i ta mogućnost.

Neophodnost postojana AbstractState klase. Dok sam implementirao StateManagera razmišljao sam da li je potrebno da sva stanja imaju zajedničku osnovnu klasu ili ne. Načelno ako bi state atribut StateManagera bio iz klase Object nikakve razlike u implementaciji ne bi bilo. Trenutni pristup obezbeđuje da svako konkretno stanje ima referencu na Kontekst koja mu je neophodna. Sa druge stane ovakva implementacija sprečava da konkretna stanja budu Singletoni.

Aktiviranje metoda koje se pokreću nakon ulaska u neko stanje. Ovom komponentom nije obuhvaćeno to što će se dešavati nakon što sistem uđe u neko stanje. To je ostavljeno da se u kontekstu implementira. Ne bi bilo loše da se i taj deo doda u komponentu kako bi obuhvatila sve što je vezano za promene stanja.

Primer

Fajl sa StateManagerom i primerom robota čistačice možete preuzeti ovde