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.

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.

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ć.