Stan State

Do czego służy State?
Jest to behawioralny wzorzec projektowy. Stan pozwala uzależnić zachowanie obiektu od stanu w którym się znajduje.
Stwórzmy przykładowy program CD Player, aby uchwycić istotę wzorca State. Aplikacja służy do odtwarzania muzyki z płyty CD. Aby to uczynić trzeba wykonać wszystkie 3 akcje w odpowiedniej kolejności:
- włożyć płytę,
- wybrać utwór,
- włączyć start.
Przykładowy program CD Player
W programie utworzyłem klasę CD_Player, a w niej powyższe trzy metody: włóż płytę (insertCD), wybierz utwór (selectTrack) oraz naciśnij start (pressStart).
Aby posłuchać utworu z dysku CD wystarczy utworzyć obiekt cd_Player i wywołać kolejno te metody.
public class Main {
public static void main(String[] args) {
CD_Player cd_player = new CD_Player();
cd_player.insertCD();
cd_player.selectTrack();
cd_player.pressStart();
}
}
public class CD_Player {
public void insertCD () {
System.out.println(„Włożono płytę CD”);
}
public void selectTrack () {
System.out.println(„Wybrano utwór”);
}
public void pressStart() {
System.out.println(„Zagrano utwór”);
}
}
Włożono płytę CD
Wybrano utwór
Zagrano utwór
Kolejność ma znaczenie
Wszystko zgodnie z zakładaną kolejnością, jednakże my nie możemy zakładać, że użytkownik będzie wiedział w jakiej kolejności ma wykonać te metody aby odtworzyć utwór z płyty. Gdy wykonamy te metody w innej kolejności, to w odpowiedzi nic się nie zmieni, a powinno. Gdy klikniemy start, gdy niema w odtwarzaczu dysku CD powinniśmy dostać inny komunikat, niż gdy robimy to w odpowiedniej kolejności.
Aby to osiągnąć dodajemy enuma: State, który przyjmuje każdy ze stanów:
NO_CD, CD_INSERTED, TRACK_SELECTED, TRACK_PLAYS.
Następnie w każdej z metod używamy State w połączeniu ze switch’em. Ze względu na stan (State) wybierany jest odpowiedni komunikat. Robimy switch’a dla każdej w metod, w każdym z nich rozpatrujemy każdy stan. Łatwo policzyć, że w naszym przykładzie, trzeba rozpatrzyć 3x4=12 możliwości. Poniżej pokazuję zmienioną klasę CD_Player:
public class CD_Player {
State state;
public CD_Player() {
state=State.NO_CD;
}
public void insertCD () {
switch (state) {
case NO_CD:
System.out.println(„Włożono płytę CD”);
state = State.CD_INSERTED;
break;
case CD_INSERTED:
case TRACK_SELECTED:
case TRACK_PLAYS:
System.out.println(„Nie wkładaj płyty, płyta już jest”);
break;
}
}
public void selectTrack () {
switch (state) {
case NO_CD:
System.out.println(„Brak płyty CD”);
break;
case CD_INSERTED:
System.out.println(„Wybrano utwór”);
state = State.TRACK_SELECTED;
break;
case TRACK_SELECTED:
case TRACK_PLAYS:
System.out.println(„Nie wybieraj znowu, wybrano już utwór”);
break;
}
}
public void pressStart() {
switch (state) {
case NO_CD:
System.out.println(„Brak płyty CD”);
break;
case CD_INSERTED:
System.out.println(„Nie wybrano utworu”);
break;
case TRACK_SELECTED:
System.out.println(„Zagrano utwór”);
state = State.TRACK_PLAYS;
break;
case TRACK_PLAYS:
System.out.println(„Nie trzeba klikać dwa razy, muzyka już gra”);
break;
}
}
public enum State {
NO_CD, CD_INSERTED, TRACK_SELECTED, TRACK_PLAYS
}
}
"Powinno działać, gdy będą zmiany, później będziemy się martwić"
Jak Tobie to wygląda? Póki co, nie wygląda to źle, no ale pomyślmy co trzeba by zrobić, gdybyśmy chcieli obsłużyć dodatkową akcje, np. „wycisz”. Trzeba by na pewno zaimplementować kolejną metodę i obsłużyć wszystkie 3 stany oraz dodać nowy stan. Poza tym w istniejących metodach, trzeba będzie obsłużyć kolejny case. Póki mamy kilka akcji i stanów, to jest to do zrobienia, gorzej będzie gdy tych stanów lub akcji będzie kilkanaście lub kilkadziesiąt. Wówczas łatwo będzie się pomylić, o czymś zapomnieć. Naprzeciw takim problemom wychodzi właśnie State.
Stan State - uproszczony schemat działania

Na początku tworzymy klasy dla każdego stanu. Następnie tworzymy interfejs State, który implementujemy w każdej klasie. Do interfejsu dodajemy każdą akcję, dzięki temu nic nie zapomnimy, ponieważ kompilator wymusi nam implementację każdej akcji w każdym stanie.
public class Main {
public static void main(String[] args) {
CD_Player cd_player = new CD_Player();
cd_player.insertCD();
cd_player.selectTrack();
cd_player.pressStart();
}
}
public interface State {
void insertCD(CD_Player cd_player);
void selectTrack(CD_Player cd_player);
void pressStart(CD_Player cd_player);
}
public class CD_Player {
State state;
public CD_Player() {
state = new NoCDState();
}
public void insertCD() {
state.insertCD(this);
}
public void selectTrack() {
state.selectTrack(this);
}
public void pressStart() {
state.pressStart(this);
}
}
public class NoCDState implements State{
@Override
public void insertCD(CD_Player cd_player) {
System.out.println(„Płyta została włożona”);
cd_player.state = new CDInsertState();
}
@Override
public void selectTrack(CD_Player cd_player) {
System.out.println(„Włóż płytę CD!”);
}
@Override
public void pressStart(CD_Player cd_player) {
System.out.println(„Włóż płytę CD!”);
}
}
public class CDInsertState implements State{
@Override
public void insertCD(CD_Player cd_player) {
System.out.println(„Płyta została już włożona”);
}
@Override
public void selectTrack(CD_Player cd_player) {
System.out.println(„Wybrano utwór”);
cd_player.state = new TrackSelectedState();
}
@Override
public void pressStart(CD_Player cd_player) {
System.out.println(„Nie wybrano utworu”);
}
}
public class TrackSelectedState implements State{
@Override
public void insertCD(CD_Player cd_player) {
System.out.println(„Płyta została już włożona”);
}
@Override
public void selectTrack(CD_Player cd_player) {
System.out.println(„Nie wybieraj znowu, wybrano już utwór”);
}
@Override
public void pressStart(CD_Player cd_player) {
System.out.println(„Zagrano utwór”);
cd_player.state = new TrackPlaysState();
}
}
public class TrackPlaysState implements State{
@Override
public void insertCD(CD_Player cd_player) {
System.out.println(„Płyta została już włożona”);
}
@Override
public void selectTrack(CD_Player cd_player) {
System.out.println(„Nie wybieraj znowu, wybrano już utwór”);
}
@Override
public void pressStart(CD_Player cd_player) {
System.out.println(„Nie trzeba klikać dwa razy, muzyka już gra”);
}
}
Podsumowanie
Efekt mamy ten sam co w pierwszej wersji aplikacji, jednakże teraz kod jest znacznie czytelniejszy. Zamiast wielu switch’y mamy stany wyodrębnione do odrębnych klas. Kod na pewno jest łatwiej rozszerzalny i trudniej coś zepsuć.
