Command
Sissejuhatus
Enamik programme võimaldab kasutajatel teha toiminguid: vajutada nuppu, liigutada liugurit, klõpsata menüüelemendil. Tavaliselt täidetakse toiming koheselt ja sellega asi piirdub.
Mõned programmid vajavad aga enamat: võimalust toiming tagasi võtta, seda korrata, panna see hilisemaks täitmiseks järjekorda või koondada mitu toimingut üheks. Kõik see muutub lihtsaks, kui käsitleda toiminguid objektidena.
Käsumuster (Command) kapseldab toimingu - koos kõige vajalikuga selle sooritamiseks ja tagasipööramiseks - ühte objekti.
Objekt, mis toimingu käivitab, ei pea teadma, kuidas seda tegelikult tehakse.
Ta kutsub lihtsalt välja execute() meetodi.
Probleemist lähemalt
Toome näiteks fototöötlusrakenduse. Kasutaja saab reguleerida heledust, kontrasti ja küllastust ning iga muudatus peaks olema võimalik tagasi võtta.
Otsene lähenemine oleks siduda iga juhtnupp otse foto endaga:
public class PhotoEditor {
private Photo photo;
public void onBrightnessChanged(int value) {
photo.setBrightness(value);
}
public void onContrastChanged(int value) {
photo.setContrast(value);
}
// ...
}
See töötab ühe muudatuse korral, kuid sellega kaasneb mitu probleemi:
- Muudatust ei saa tagasi võtta. Heleduse muutuse tagasipööramiseks oleks vaja teada eelmist väärtust - teadmist, mida kasutajaliides ei peaks kandma.
- Filtrid (mitu muudatust, mille käivitab üks nupp) ei ole võimalikud ilma koodi dubleerimata.
- Redaktor on otseselt seotud iga fotooperatsiooniga. Uue kohanduse lisamine tähendab redaktori muutmist.
- Toiminguid ei saa järjekorda panna ega logida.
Käsumuster
Muster toob sisse käsu (Command) liidese kahe meetodiga:
public interface Command {
void execute();
void undo();
}
Iga toiming muutub eraldi klassiks, mis teab nii seda, kuidas toimingut sooritada, kui ka seda, kuidas seda tagasi võtta:
public class AdjustBrightnessCommand implements Command {
private final Photo photo;
private final int newValue;
private int previousValue;
public AdjustBrightnessCommand(Photo photo, int newValue) {
this.photo = photo;
this.newValue = newValue;
}
@Override
public void execute() {
previousValue = photo.getBrightness();
photo.setBrightness(newValue);
}
@Override
public void undo() {
photo.setBrightness(previousValue);
}
}
public class AdjustContrastCommand implements Command {
private final Photo photo;
private final int newValue;
private int previousValue;
public AdjustContrastCommand(Photo photo, int newValue) {
this.photo = photo;
this.newValue = newValue;
}
@Override
public void execute() {
previousValue = photo.getContrast();
photo.setContrast(newValue);
}
@Override
public void undo() {
photo.setContrast(previousValue);
}
}
Käskude käivitaja (invoker), mis antud kontekstis oleks siis fotoredaktor, hoiab täidetud käskude ajalugu ja kasutab seda tagasivõtmise (undo) toetamiseks:
public class PhotoEditor {
private final Deque<Command> history = new ArrayDeque<>();
public void execute(Command command) {
command.execute();
history.push(command);
}
public void undo() {
if (history.isEmpty()) {
throw new IllegalStateException("Nothing to undo");
}
history.pop().undo();
}
}
Kasutajaliides tegeleb nüüd ainult käskudega, mitte enam otseselt fotooperatsioonidega:
Photo photo = new Photo("sunset.jpg");
PhotoEditor editor = new PhotoEditor();
editor.execute(new AdjustBrightnessCommand(photo, 20)); // increases brightness
editor.execute(new AdjustContrastCommand(photo, 15)); // increases contrast
editor.undo(); // reverts contrast change
editor.undo(); // reverts brightness change
Antud töövoog näeks selline välja:
Pane tähele
- Käivitaja (
PhotoEditor) ei tea midagi konkreetsetest fototöötluse toimingutest. See teab ainult, et käskudel on meetodidexecute()jaundo(). - Foto (
Photo) ei tea midagi redaktorist. See lihtsalt pakub oma toiminguid tavaliste meetoditena. - Iga käsk on iseseisev.
See sisaldab viidet fotole, millele see rakendub, ning vajaduse korral andmeid, mis on vajalikud toimingu tagasipööramiseks (eelmine väärtus käsus
AdjustBrightnessCommand).
Makrokäsud
Üks kasulik tagajärg sellele, et käske käsitletakse objektidena, on see, et käskude loend on ise samuti kehtiv käsk. Seda nimetatakse mõnikord makrokäsuks või koondkäsuks:
public class MacroCommand implements Command {
private final List<Command> commands = new ArrayList<>();
public void add(Command command) {
commands.add(command);
}
@Override
public void execute() {
for (Command command : commands) {
command.execute();
}
}
@Override
public void undo() {
// undo in reverse order
ListIterator<Command> it = commands.listIterator(commands.size());
while (it.hasPrevious()) {
it.previous().undo();
}
}
}
Filtri eelseadistus ühendab mitu üksikut kohandust üheks:
MacroCommand vintageFilter = new MacroCommand();
vintageFilter.add(new AdjustBrightnessCommand(photo, -10));
vintageFilter.add(new AdjustContrastCommand(photo, 15));
vintageFilter.add(new AdjustSaturationCommand(photo, -30));
editor.execute(vintageFilter); // applies all three adjustments at once
editor.undo(); // reverses all three in one step
Redaktor käsitleb makrot täpselt nagu mistahes muud käsku. Tühistamine pöörab kõik kolm kohandust tagasi vastupidises järjekorras.
Kasutusjuhud
Käsumuster on sobilik, kui:
- Vajad tühistamise/taaste (undo/redo) funktsionaalsust
- Soovid koostada makrotoiminguid väiksemate toimingute jadadest.
- Sul on vaja toiminguid järjekorda panna, ajastada või logida.
- Soovid lahutada objekti, mis tegevuse algatab, objektist, mis selle tegelikult teostab.
See lisab klasse, seega ei ole mõistlik seda kasutada lihtsate, ühekordsete toimingute puhul, mida ei ole vaja tühistada. Kui tühistamine, makrod või järjekorrastamine ei ole vajalikud, on otsene meetodi väljakutse selgem.