Stan

Wzorce projektowe

  |   9 min czytania

Zastosowanie

Stan (ang. State) (wzorzec behawioralny) definiuje rodzinę stanów jakie może przyjąć obiekt kontekstu w trakcie działania programu i na podstawie zmiany stanu obiektu kontekstu umożliwia zmianę jego zachowanie. Stany przypominają automat stanów, tzn. w zależności od bieżącego stanu podejmowana jest przez niego odpowiadająca mu akcja oraz może nastąpić zmiana stanu obiektu kontekstu. O tym czy stan obiektu kontekstu zostanie zmieniony decydują klasy stanu (w zależności od podjętej akcji znają one swojego następnika). Szczegóły implementacji podejmowanych akcji są ukryte dla klienta i delegowane do klas stanu. Dzięki czemu upraszcza się kod kontekstu, eliminuje instrukcje warunkowe oraz kod realizacji zadań w zależności od stanu jest organizowany w osobne klasy.

Ograniczenia

Wzorzec Stan jest strukturalnie podobny do wzorca Strategia. W związku z czym może zachodzić obawa o mylenie obu wzorców, które służą realizacji innych celów. W przypadku, gdy liczba stanów jest mała lub częstotliwośc zmiany stanu jest niska użycie wzorca pomimo słusznych przesłanek może być przerostem formy nad treścią.

Użycie

Wzorzec ten podobnie jak Adapter wykorzystuje mechanizm delegowania operacji do innego obiektu. Ponadto jego struktura jest podobna do struktury wzorca Strategia oraz Most. Jednakże celem wzorca Most jest zaprojektowanie odpowiedniej struktury klas projektu, Strategia skupia się na zmienności implementacji tych samych lub podobnych zadań, natomiast Stan realizuje zmienność zachowania obiektu w zależności od jego stanu wewnętrznego. Stan ma zastosowanie w sytuacjach, gdzie obiekt kontekstu może przyjmować różne stany w trakcie działania programu, a realizacja żądań jest zależna od stanu wewnętrznego obiektu.

Implementacja

Klasa kontekstu Context przechowuje referencję do obiektu bieżącego stanu (kontekstu) State. Metody kontekstu delegują wykonanie pracy do obiektu stanu, który wykonuje zadanie oraz zmienia referencję do obiektu stanu. Klasy stanów implementują interfejs State, którego metody są często powiązane z metodami o tej samej nazwie w klasie kontekstu. Ponadto klasa Context umożliwia zmianę oraz dostęp do stanu. Klasy stanów nierzadko są realizowane z użyciem Singletonu.

Stan diagram

Poniższy listing prezentuje implementację wzorca Stan wykorzystywanego w klasie Context, która może przyjąć jeden ze stanów: State1, State2 lub State3.

public class State1 implements {

    public void handle(Context context) {
        //do some specific action for State1
        context.action1();
        context.setState(new State2());
    }
}

public class State2 implements {

    public void handle(Context context) {
        //do some specific action for State2
        context.action2(true);
        context.setState(new State3());
    }
}

public class State3 implements {

    public void handle(Context context) {
        //do some specific action for State3
        context.action2(false);
        context.setState(new State1());
    }
}

public class Context {

    private State state;

    public Context() {
        //set default state
        state = new State1(this);
    }

    public void request() {
        state.handle(this);
    }

    public void action1() {
        //do something specific for action1
    }

    public void action2(boolean enabled) {
        //do something specific for action2
    }

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

    public State getState() {
        return state;
    }
}

interface State {

    void handle(Context context);
}

Klient wykonuje operacje na obiekcie kontekstu przez co zmienia się także jego stan. W zależności od bieżącego stanu podjęta akcja na to samo żądanie może być realizowana w różny sposób.

//start state is State1
Context context = new Context();

//user request operation
context.request(); //State1 handle the action
//operation is done, state has changed

//user again request operation many times during the state is changing
context.request(); //State2 handle the action
context.request(); //State3 handle the action
context.request(); //again State1 handle the action

Przykład

MusicPlayer jest odtwarzaczem muzyki, który oferuje podstawową funkcjonalność dla tego typu aplikacji. Użytkownik może stworzyć i odtwarzać własną playlistę lub wybrać playlistę na podstawie albumu bądź artysty. Utwory są odtwarzane wg kolejności z playlisty, a nawigacja między nimi oraz widok panelu użytkownika są zależne od stanu odtwarzania utworu. Poniższy listing przedstawia sposób realizacji nawigowania między utworami przy wykorzystaniu wzorca Stan dla stanów odtwarzane (PlayState) oraz wstrzymane (StopState).

public class PlayState implements State {

    //do some things in every methods and run player's method or modify player if needed

    @Override
    public void play(Player player) {
        player.stopTrack();
        player.setState(new StopState());
    }

    @Override
    public void next(Player player) {
        player.nextTrack();
    }

    @Override
    public void previous(Player player) {
        player.playTrack();
    }
}

public class StopState implements State {

    //do some things in every methods and run player's method or modify player if needed

    @Override
    public void play(Player player) {
        player.playTrack();
        player.setState(new PlayState());
    }

    @Override
    public void next(Player player) {
        player.nextTrack();
    }

    @Override
    public void previous(Player player) {
        player.previousTrack();
    }
}

public class Player {

    private State state;
    private List<Track> tracks;
    private int currentTrack;
    //other fields like system or music controllers

    public Player(List<Track> tracks){
        this.tracks = tracks;
        currentTrack = 0;
        currentTime = 0;
        state = new StopState();
    }

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

    public State getState(){
        return state;
    }

    public void play() {
        state.play(this);
    }

    public void next() {
        state.next(this);
    }

    public void previous() {
        state.previous(this);
    }

    public void playTrack() {
        //play the currentTrack
        //change UI button
    }

    public void stopTrack() {
        //stop the currentTrack
        //change UI button
    }

    public void nextTrack() {
        //stop the CurrentTrack
        if(currentTrack < tracks.size() - 1)
            currentTrack++;
        else
            currentTrack = 0;
        //play the currentTrack
        //change UI info
    }

    public void previousTrack() {
        if(currentTrack > 0)
            currentTrack = tracks.size() - 1;
        else
            currentTrack--;
        //play the currentTrack
        //change UI info
    }
}

interface State {

    void play(Player player);
    void next(Player player);
    void previous(Player player);
}

Użytkownik tworzy własną playlistę, a następnie odtwarza, wstrzymuje oraz nawiguje się między utworami.

//user create playlist
List<Track> playlist = new ArrayList<>();
playlist.add(new Track("Depeche Mode", "Enjoy the silence", 250, "dm01.mp3"));
playlist.add(new Track("Abba", "Dancing Queen", 195, "abba07.mp3"));
playlist.add(new Track("Ben Howard", "Keep your head up", 217, "bh04.mp3"));
playlist.add(new Track("Of Monsters and Men", "Yellow Light", 184, "omm12.mp3"));

//user choose playlist to play right now
Player player = new Player(playlist);

//player is initialized and ready to play the first song
player.play(); //Enjoy the silence is playing (StopState)
player.next(); //Dancing Queen is playing (PlayState)
player.stop(); //Dancig Queen is pausing (StopState)
player.previous(); //Enjoy the sielence is loaded again (StopState)
player.play(); //Enjoy the silence is playing (PlayState)
player.previous(); //Enojy the sielence is playing from the beginning (PlayState)

Biblioteki

Realizacja wzorca Stan spoczywa na programiście i polega na identyfikacji zbioru stanów dla obiektu kontekstu oraz implementacji zachowań klas stanów.