Liigu peamise sisu juurde

Mustrite sünergia

Sissejuhatus

Eelmistes peatükkides käsitleti nelja mustrit eraldi, igaüks lahendas üht konkreetset probleemi. Reaalsetes süsteemides ei ilmu probleemid ükshaaval.

Vaatleme muusikamängija rakendust. Selle nõuded on lihtsad ja otsekohesed:

  • Mängija võib olla erinevates olekutes: muusika kas mängib, on pausil või täienisti peatatud. Teatud tegevused on mõistlikud ainult teatud olekutes, näiteks peatatud mängijat ei saa pausile panna.
  • Kasutaja tegevused (mängi, paus, järgmine) peavad toetama tagasivõtmist.
  • Kasutajaliides peab peegeldama iga olekumuutust: mängimise nupp muutub, edenemisriba uueneb, loo pealkiri muutub jne.
  • Taasesituse järjekord võib varieeruda järjest esitamise pealt juhuesituse peale.

Neli nõuet, millest igaüks vastab mustrile, mida juba tunneme. Mis siin huvi pakub ei ole see, et iga muster esineb omaette, vaid see, kuidas need omavahel seostuvad.

Süsteemist lähemalt

Süsteemi ülesehitus on lihtne: Player, millel on hetkel mängitav lugu, olek ja nimekiri kasutajaliidese komponentidest, mis seda jälgivad.

public class Player {
private Track currentTrack;
private PlayerState state;
private PlaybackStrategy playbackStrategy;
private final List<PlayerObserver> observers = new ArrayList<>();

public Player(PlaybackStrategy playbackStrategy) {
this.playbackStrategy = playbackStrategy;
this.state = new StoppedState(this);
}

// Called by state classes to transition
public void setState(PlayerState newState) {
this.state = newState;
notifyObservers();
}

public void addObserver(PlayerObserver observer) {
observers.add(observer);
}

private void notifyObservers() {
for (PlayerObserver observer : observers) {
observer.onPlayerChanged(this);
}
}

// Delegated to current state
public void play() { state.play(); }
public void pause() { state.pause(); }
public void stop() { state.stop(); }

public Track nextTrack() {
return playbackStrategy.next(currentTrack);
}

public Track getCurrentTrack() { return currentTrack; }
public void setCurrentTrack(Track track) { this.currentTrack = track; }
public String getStateName() { return state.getStateName(); }
}

Isolatsioonis pole siin midagi ebatavalist. Kõik neli mustrit on esindatud mõne välja või mehhanismina: PlayerState (State), PlaybackStrategy (Strategy), List<PlayerObserver> (Observer), ja käsud tulevad eraldi.

Vaatame täpsemalt, kuidas need koos töötavad.

1. Olek määrab, mida käsud võivad teha

Iga käsk kontrollib enne täitmist, kas praegune olek lubab antud tegevust. Näiteks canExecute() meetod pöördub mängija oleku poole:

public class PlayCommand implements Command {
private final Player player;
private Track previousTrack;
private String previousStateName;

public PlayCommand(Player player) {
this.player = player;
}

@Override
public boolean canExecute() {
// the command does not hardcode "if state == STOPPED"
// it asks the state itself
return player.getState().canPlay();
}

@Override
public void execute() {
previousTrack = player.getCurrentTrack();
previousStateName = player.getStateName();
player.play();
}

@Override
public void undo() {
// restore whatever was happening before
player.setState(player.resolveState(previousStateName));
player.setCurrentTrack(previousTrack);
}
}

Käsk ei sisalda tingimusi if (state == STOPPED ...) stiilis. See küsib olekult muusikamängijalt endalt. See tähendab, et reeglid elavad ühes kohas - olekuklassides - ja kõik käsud kasutavad neid ilma dubleerimiseta.

Uue oleku lisamisel ei pea käske muutma. Kui lisatakse uus olek (näiteks BufferingState, kus taasesitus ei saa alata), ei pea ühtegi olemasolevat käsku muutma. BufferingState.canPlay() meetod tagastab false ning kõik käsud, mis kutsuvad canPlay(), arvestavad sellega automaatselt.

2. Käsud muudavad olekut, mis omakorda teavitab vaatlejaid

Käsu täide viimisel kutsutakse mängija meetodeid. Mängija omakorda delegeerib tegevuse üle olekule. Olek viib käsu täide ning lõpptulemusena muudab olekut player.setState(...) meetodi kaudu, setState omakorda teavitab vaatlejaid.

Käsk ei tea vaatlejatest midagi. Olek ei tea käskudest midagi. Aga ahel ühendab need.

Vaatleja liides vajab sisendina ainult viidet olemasolevale mängijale, et vaatlejad saaksid mängijalt endale sobivaid andmeid pärida:

public class PlayButtonObserver implements PlayerObserver {
private final Button playButton;

@Override
public void onPlayerChanged(Player player) {
boolean isPlaying = player.getStateName().equals("Playing");
playButton.setIcon(isPlaying ? "⏸" : "▶");
}
}

Nupp uueneb ilma otsese sidumiseta.

3. Strateegia on sõltumatu

PlaybackStrategy määrab järjekorras järgmise loo. See ei sõltu olekust, vaatlejatest ega käskude ajaloost.

public class SkipCommand implements Command {
private final Player player;
private Track previousTrack;

public SkipCommand(Player player) {
this.player = player;
}

@Override
public boolean canExecute() {
return player.getState().canSkip();
}

@Override
public void execute() {
previousTrack = player.getCurrentTrack();
player.setCurrentTrack(player.nextTrack()); // strategy decides the track
}

@Override
public void undo() {
player.setCurrentTrack(previousTrack);
}
}

player.nextTrack() delegeerib misiganes strateegiale praeguse esitusloendi. SequentialStrategy pealt ShuffleStrategy peale üleminek muudab SkipCommand-i käitumist, ilma et käsk sellest ise teadlik oleks.

Strateegia on antud mustrite seas ainus, mis ei ole otseselt seotud teistega. See on tahtlikult nii - see peegeldab fakti, et esitusjärjekord on olekust, ajaloost ja kasutajaliidesest eraldiseisev üksus.

Kõik jupid korraga koos

Tulemusena kasutaja näeb ainult pinnapealset loogikat ehk mida on võimalik teha:

Player player = new Player(new ShuffleStrategy(trackList));
player.addObserver(new PlayButtonObserver(playButton));
player.addObserver(new ProgressBarObserver(progressBar));
player.addObserver(new NowPlayingObserver(titleLabel));

PlayerController controller = new PlayerController();

// User presses play
controller.execute(new PlayCommand(player));

// User skips a track
controller.execute(new SkipCommand(player));

// User regrets the skip
controller.undo();

// User switches to sequential playback while the player is running
player.setPlaybackStrategy(new SequentialStrategy(trackList));

Kokkuvõtteks

Iga muster lahendab üht kindlat probleemi:

ValdkondMuster
Millised tegevused on lubatudState
Tagasipööratavad tegevusedCommand
Reageerimine muudatusteleObserver
Vahetatavad algoritmidStrategy

Mustrid omavahel ei kattu. Koostöös need lahendavad ära kogu probleemi ulatuse ilma, et ükski klass muutuks liiga suureks.

Olulisem on aru saada, mida siin saavutati ja miks. Nõuded kirjeldasid süsteemi - süsteem pidi koosnema olekust sõltuvast käitumisest, tagasipööratavatest tegevustest, reageeritavast kasutajaliides ja vahetatavatest algoritmidest. Mustrid on lihtsalt väljakujunenud nimetused nendele neljale kujule. Antud probleemi raames mustrid kujunesid nõuetest isenesest.

Mustrite tundmine on kasulik, sest see annab sõnavara ja läbiproovitud struktuuri, mille poole pöörduda. Aga lähtepunkt on alati probleem. Kui nõue ütleb "kasutaja peab saama seda tühistada", tunned selle ära kui käsumustrina. Kui nõue ütleb „mitu kasutajaliidese osa peavad selle väärtusega sünkroonis püsima”, tunned selle ära kui vaatlejamustrina. Mustrid ei ütle, mida ehitada. Need aitavad kirjeldada seda, mida oled juba ehitanud ja millest oled aru saanud.