SW_12
Vzor Příkaz - Command Vzor Návštěvník - Visitor
1
Vzor Příkaz - Command • Problém: • Standardní cesta pro vykonání metody je její vyvolání. • V některých situacích nejsme schopni řídit načasování (timing) kontextu, ve kterém by se měly metody volat. • Dalším problémem je flexibilní zpracování návratové hodnoty metody.
2
Vzor Příkaz - Command • Kontext: • Jak ovládat řadu zařízení přes dálkový ovladač. • Požadujeme, aby ovladač měl jednoduché funkce a abychom byli schopni modifikovat jím ovládaná zařízení (přidat nové, odebrat původní).
3
Vzor Příkaz • Řešení: • Zapouzdření operace do formy objektu typu command. • Oddělení požadavku na danou akci od objektu, který skutečně akci provede. • Návrhový vzor Command právě oddělí uvedené části od sebe.
4
Vzor Příkaz • Vzor si vysvětleme na příkladu dálkového ovladače (remote controller), který bude skutečným žadatelem (requester) provedení konkrétních akcí, které zvolíme stiskem daných tlačítek ovladače. • Požadavek pak provede konkrétní třída daného zařízení.
5
Vzor Příkaz • Jednoduchými požadovanými operacemi mohou být požadavky na rozsvícení, zhasnutí světla, zapnutí, vypnutí televizoru. • Objekt command pak bude provádět tuto konkrétní operaci. • Bude třeba tedy vytvořit řadu objektů command pro každé tlačítko vzdáleného ovladače, které pak zabezpečí vykonání konkrétní operace.
6
Postup 1. Klient vytvoří objekt command. 2. Klient vykoná metodu setCommand() k uložení objektu command do invokátoru (vyvolávače). 3. Později klient požádá invokátor o provedení objektu command. • Projevuje se snaha ukládat tyto objekty command do nějaké kolekce a pak je vyvolat. • Využití při událostmi řízeném programování. 7
Postup • Za zmínku stojí, že poté, co je objekt command nahrán do invokátoru (metodou setCommand()), může být objekt command využit jednou nebo vícekrát, nebo také zrušen či nahrazen jiným objektem command.
8
public interface Command { public void execute(); }
Poznámky
Vytvoření prvního objektu command Předtím, než vytvoříme první objekt command, který bude rozsvěcet světlo, deklarujeme nejdříve rozhraní, které bude využívat vzdálený ovladač.
public class LightOnCommand implements Command { private Light light; public LightOnCommand(Light light) { // konstruktor this.light = light; } public void execute() { light.on(); } }
Poznámky
Jediná metoda rozhraní se jmenuje execute(). Následuje třída „konkrétní“ Command (LightOnCommand), která implementuje rozhraní Command.
public class Light { // konstruktor public Light() { } public void on() { System.out.println("Light is on"); } public void off() { System.out.println("Light is off"); } }
Poznámky
Vidíme, že třída Light deklaruje datový atribut light (světlo), který je nastaven v konstruktoru. Implementace metody exekute() pak vyvolá metodu on() třídy Light, čímž se rozsvítí světlo. Třída Light (světlo) má jistě i metodu off(), kterou se světlo zhasne.
public class SimpleRemoteControl { Command slot; // místo pro zařízení public SimpleRemoteControl() {} // konstruktor public void setCommand(Command command) { slot = command; } public void buttonWasPressed() { slot.execute(); } }
Poznámky Použití objektu command Předpokládejme, že máme dálkový ovladač s jedním tlačítkem a odpovídajícím místem (pozicí, slotem) pro zařízení, které budeme dálkovým ovladačem řídít. Metoda setCommand() nastavuje konkrétné objekt command do proměnné slot. Při změně objektu command, musíme tuto metodu vyvolat znova. Metoda buttonWasPressed() je vyvolána vždy při stisku tlačítka.
public class RemoteControlTest { public static void main(String[] args) { SimpleRemoteControl remote = new SimpleRemoteControl(); // objekt light je příjemcem požadavku Light light = new Light(); LightOnCommand lightOn = new LightOnCommand(light); remote.setCommand(lightOn); remote.buttonWasPressed(); } }
Poznámky
Jednoduchý test funkčnosti je pak následující: Třída RemoteControlTest představuje klienta ve vzoru Command. Objekt remote je náš invoker (ten, kdo se něčeho dovolává) a je mu předán konkrétní objekt commanderu v metodě setCommand().
Vzor Příkaz - definování • Vzor Command zapouzdřuje pořadavek (request) jako objekt a tím nám dovoluje parametrizovat další objekty s odlišnými požadavky, frontovat nebo zaznamenávat požadavky a podporovat operaci undo (zrušení provedené operace).
14
Vzor Příkaz - definování • Víme, že objekt command svazuje dohromady množinu akcí na konkrétním příjemci. • Příjemcem bylo světlo, ale mohou to být např. garážová vrata, u kterých můžeme požadovat akce: zavření, otevření, zastavení, zhasnutí světla v garáži, rozsvícení světla v garáži. • Abychom toho dosáhli, vzor pakuje akce a příjemce do jednoho objektu, který vystavuje jednu metodu execute(). 15
Vzor Příkaz - definování • Když je objekt vyvolán, metoda execute() způsobí vyvolání akcí příjemce. • Z vnějšího pohledu žádné jiné objekty skutečně neví, jaké akce jsou na příjemci vykonány; tyto objekty pouze ví, že když vyvolají metodu execute(), jejich požadavek bude obsloužen.
16
Vzor Příkaz - definování • Podíváme se blíže na garáž. Vidíme, že se jedná celkem o pět akcí. • Musíme proto doplnit odpovídající objekty command pro každou akci.
17
public class GarageDoor { public GarageDoor() { } public void up() { System.out.println("Garage Door is Open"); } public void down() { System.out.println("Garage Door is Closed"); } public void stop() { System.out.println("Garage Door is Stopped"); } public void lightOn() { System.out.println("Garage light is on"); } public void lightOff() { System.out.println("Garage light is off"); } }
Poznámky
public class GarageDoorOpenCommand implements Command { GarageDoor garageDoor; public GarageDoorOpenCommand(GarageDoor garageDoor) { this.garageDoor = garageDoor; } public void execute() { garageDoor.up(); } }
Poznámky
public class RemoteControlTest { public static void main(String[] args) { SimpleRemoteControl remote = new SimpleRemoteControl(); GarageDoor garageDoor = new GarageDoor(); GarageDoorOpenCommand garageOpen = new GarageDoorOpenCommand(garageDoor); remote.setCommand(garageOpen); remote.buttonWasPressed(); } }
Poznámky
Diagram tříd
21
Aplikace • Rozhraní Command je rozhraní v našem případě zatím pouze s metodou execute(). • Třída ConcreteCommand je v našich příkladech reprezentovaná třídami LightOnCommand a GarageDoorOpenCommand. • Třídu Receiver (příjemce) reprezentují konkrétní třídy Light (světlo) a GarageDoor. Třída Invoker s metodou setCommand() je reprezentovaná třídou SimpleRemoteControl. 22
Aplikace • Třída Client je reprezentovaná třídou s hlavní metodou, tedy třídou RemoteControlTest. • Třída Client je zodpovědná za vytváření objektů tříd ConcreteCommand a nastavení jejich příjemce (receiver). • Třída Invoker uchovává objekt command a v daném místě žádá objekt command o provedení požadavku, vyvoláním jeho metody execute().
23
Aplikace • Třída ConcreteCommand definuje vazbu mezi akcí a příjemcem. • Invoker vyvolá požadavek vyvoláním metody execute() a objekt concreteCommand to vykoná vyvoláním jedné nebo více akcí na příjemci. • Příjemce (receiver) zná, jak provést konkrétní požadavek. • Jako příjemce může sloužit libovolná třída.
24
Aplikace • Nyní se podíváme, jak bychom řešili vzdálený ovladač pro sedm zařízení. • Pokud budeme ovládat např. světlo v kuchyni a světlo v obývacím pokoji, nutně potřebujeme dva konkrétní objekty command, jeden pro světlo v kuchyni a druhý pro světlo v obývacím pokoji. • Vzor Command to neumí rozlišit.
25
Aplikace • Další věc, kterou musíme zavést, jsou pole pro nastavení jednotlivých zařízení. • Tato pole povedeme dvoje a to pro zapnutí a vypnutí, což jsou nejzákladnější operace.
26
public class RemoteControl { Command[] onCommands; // pole pro zapnutí Command[] offCommands; // pole pro vypnutí public RemoteControl() { onCommands = new Command[7]; offCommands = new Command[7]; Command noCommand = new NoCommand(); for (int i = 0; i < 7; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } } public void setCommand(int slot, Command onCommand, Command offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } public void onButtonWasPushed(int slot) { onCommands[slot].execute(); }
Poznámky
public void offButtonWasPushed(int slot) { offCommands[slot].execute(); } public String toString() { StringBuffer stringBuff = new StringBuffer(); stringBuff.append("\n---- Remote Control -----\n"); for (int i = 0; i < onCommands.length; i++) { stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName() " + offCommands[i].getClass().getName() + " + "\n"); } return stringBuff.toString(); } }
Poznámky
public class NoCommand implements Command { public void execute() { } }
Poznámky
Vzdálený ovladač ovládá sedm jednoduchých zařízení (zapnutí / vypnutí). Na začátku třídy nastavíme všechny přepínače na objekt noCommand, jehož třídu uvádíme. To znamená, že pokud nějaký přepínač nebude definovaný, implicitně bude jako noCommand, tedy prázdná metoda execute().
Aplikace • Metoda setCommand() má tři parametry pro uchování všech informací. • Prvním parametrem je slot, který tvoří index pro uložení informací v polích. • Další dva parametry jsou pro konkrétní objekty command pro zapnutí a pro vypnutí. • Metoda toString() vypisuje všechna nastavení této třídy.
30
public class StereoOnWithCDCommand implements Command { Stereo stereo; public StereoOnWithCDCommand(Stereo stereo) { this.stereo = stereo; } public void execute() { stereo.on(); stereo.setCD(); stereo.setVolume(11); } }
Poznámky Třída StereoOnWithCDCommand může mít pak následný kód:
public class Stereo { String location; public Stereo(String location) { this.location = location; } public void on() { System.out.println(location + " stereo is on"); } public void off() { System.out.println(location + " stereo is off"); } public void setCD() { System.out.println(location + " stereo is set for CD input"); } public void setDVD() { System.out.println(location + " stereo is set for DVD input"); }
Poznámky Tato třída konkrétního commandu spouští stereo, spouští CD přehravač s hlasitistí 11. Třída příjemce (receiver) má následující kód.
public void setRadio() { System.out.println(location + " stereo is set for Radio"); } public void setVolume(int volume) { // code to set the volume // valid range: 1-11 (after all 11 is better than // 10, right?) System.out.println(location + " Stereo volume set to " + volume); } }
Poznámky
public class RemoteLoader { public static void main(String[] args) { RemoteControl remoteControl = new RemoteControl(); Light livingRoomLight = new Light("Living Room"); Light kitchenLight = new Light("Kitchen"); CeilingFan ceilingFan= new CeilingFan("Living Room"); GarageDoor garageDoor = new GarageDoor(""); Stereo stereo = new Stereo("Living Room"); LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight); LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight); LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight); LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight); CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan); CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan); GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor); GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor);
Poznámky K otestování našeho postupu pak bude sloužit třída RemoteLoader.
StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo); StereoOffCommand stereoOff = new StereoOffCommand(stereo); remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff); remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff); remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff); remoteControl.setCommand(3, stereoOnWithCD, stereoOff); System.out.println(remoteControl); remoteControl.onButtonWasPushed(0); remoteControl.offButtonWasPushed(0); remoteControl.onButtonWasPushed(1); remoteControl.offButtonWasPushed(1); remoteControl.onButtonWasPushed(2); remoteControl.offButtonWasPushed(2); remoteControl.onButtonWasPushed(3); remoteControl.offButtonWasPushed(3); } }
Poznámky
Aplikace • Jak jste si všimli, objekt třídy NoCommand je příkladem null objektu. • Ten také slouží k návratu objektu z metody, když „nemáme“ co poslat zpět.
36
public interface Command { public void execute(); public void undo(); }
Poznámky Přidání metody undo do rozhraní Command Když rozhraní Command podporuje metodu undo, musíme ji doplnit nejdříve do rozhraní.
public class LightOnCommand implements Command { Light light; public LightOnCommand(Light light) { this.light = light; } public void execute() { light.on(); } public void undo() { light.off(); } }
Poznámky Teď se podíváme, jak budeme metodu undo() implementovat do tříd konkrétních commandů. Ukážeme si to na příkladu rozsvícení a zhasnutí světla.
public class LivingroomLightOffCommand implements Command { Light light; public LivingroomLightOffCommand(Light light) { this.light = light; } public void execute() { light.off(); } public void undo() { light.on(); } }
Poznámky Vidíme, že operace on() a off() jsou opačné. Ještě musíme opravit třídu RemoteControl.
public class RemoteControl { Command[] onCommands; Command[] offCommands; Command undoCommand; public RemoteControl() { onCommands = new Command[7]; offCommands = new Command[7]; Command noCommand = new NoCommand(); for(int i=0;i<7;i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } undoCommand = noCommand; } public void setCommand(int slot, Command onCommand, Command offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; }
Poznámky
public void onButtonWasPushed(int slot) { onCommands[slot].execute(); undoCommand = onCommands[slot]; } public void offButtonWasPushed(int slot) { offCommands[slot].execute(); undoCommand = offCommands[slot]; } public void undoButtonWasPushed() { undoCommand.undo(); } public String toString() { StringBuffer stringBuff = new StringBuffer(); stringBuff.append("\n---- Remote Control -----\n"); for (int i = 0; i < onCommands.length; i++) { stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName() + " " + offCommands[i].getClass().getName() + "\n"); } stringBuff.append("[undo] " + undoCommand.getClass().getName() + "\n"); return stringBuff.toString(); } }
Poznámky
public class RemoteLoaderTest { public static void main(String[] args) { RemoteControl remoteControl = new RemoteControl(); Light livingRoomLight = new Light("Living Room"); LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight); LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight); remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff); remoteControl.onButtonWasPushed(0); remoteControl.offButtonWasPushed(0); System.out.println(remoteControl); remoteControl.undoButtonWasPushed(); remoteControl.offButtonWasPushed(0); remoteControl.onButtonWasPushed(0); System.out.println(remoteControl); remoteControl.undoButtonWasPushed(); } }
Poznámky Objekty command pak ještě mohou být ukládány do kolekcí a pak spouštěny najednou.
Shrnutí vzoru Příkaz • Tento vzor má řadu uplatnění, např. ošetřování chybových stavů, práce s událostmi řízených programech. • Operace, která se má provést je „zabalena do objektu“ a pak se s ní dá pružně manipulovat. Klient je odstíněn od konkrétní implementace metody execute(). • Konkrétní metoda je vybrána podle typu objektu, který metodu execute() aktuálně vyvolává. 43
Shrnutí vzoru Příkaz • Navíc možnost připojení objektu třídy noCommand a metoda undo(). • Konkrétní metoda je vybrána podle typu objektu, který metodu execute() aktuálně vyvolává. • Navíc jsme viděli, jak se dá připojit objekt třídy noCommand a metoda undo().
44
Vzor Command - příkaz • Příkazy vzoru Command se dají uložit do objektu třídy kolekce (např. Register) kvalifikovaného na objekt rozhraní Command. • To pak vede k tomu, že se dá vytvořit dynamický seznam požadavků, které se postupně provádějí.
45
Návštěvník - Visitor • Kontext: • Je daná základní hierarchie tříd daného problému, do které není možné dělat změny. • Může to být hierarchie tříd dodaná jiným dodavatelem. • Jako změny myslíme dodání dalších polymorfických metod, které by rozšířily funkčnost stávající hierarchie tříd.
46
Návštěvník - Visitor • Problém: • Je třeba dodat další metody do hierarchie tříd bez fyzického přidání daných metod do každé třídy uvedené hierarchie. • Řešení: • Řešení přináší návrhový vzor Visitor (návštěvník), který dovoluje vytvoření oddělené hierarchie tříd typu Visitor k virtualizaci operací prováděných na původní hierarchii tříd. 47
Konkrétní příklad - diagram tříd
48
Návštěvník - Visitor • Neměnnou strukturu tříd představují třídy na levé straně obrázku, tedy třídy Tvar, Kruh a Ctverec. • Na pravé straně obrázku je pak hierarchie tříd, které se využívají na rozšíření funkčnosti stávající hierarchie. • Jedná se nám o to, že chceme doplnit metody pro výpočet obsahu a obvodu daných obrazců.
49
Návštěvník - Visitor • Postup při použití vzoru je následující: • Ve stávající (neměnné) hierarchii tříd doplníme metodu accept() s argumentem instancí rozhraní Visitor do každé třídy. • Abstraktní třída Tvar deklaruje metodu accept() jako abstraktní a její podtřídy ji pak implementují podle svých skutečných potřeb. • Formální tvar je stejný, jen pseudoproměnná this je různá.
50
Poznámky
public double accept(Visitor v) { return v.visit(this); }
•
Podle počtu rozšiřujících metod, o které chceme rozšířit stávající hierarchii tříd, doplníme třídy implementující rozhraní Visitor. V našem případě se jedná o třídy VisitObsah a VisitObvod, protože chceme doplnit operace počítající obsah a obvod obrazců.
public class VisitObvod implements Visitor{ public double visit(Kruh c) { return 2 * Math.PI * c.getPolomer(); } public double visit(Ctverec s){ return 4 * s.getStrana(); } }
Poznámky
Dále musíme vytvořit ve třídách VisitObsah a VisitObvod tolik metod visit(), kolik máme různých obrazců. Protože máme dva obrazce (Square a Circle), stačí nám dvě metody visit().
Návštěvník - Visitor • Jedna metoda visit() počítá obvod kruhu a druhá metoda visit počítá obsah kruhu. • Pro větší názornost uvedeme celý kód jednoduchého příkladu:
53
public abstract public public public }
class Tvar { abstract void vykresli(); abstract void smaz(); abstract double accept(Visitor v);
public class Kruh extends Tvar{ private int polomer; public Kruh() { polomer = 16; } public double accept(Visitor v) { return v.visit(this); } public int getPolomer() { return polomer; } public void vykresli() { System.out.println("Kruh.vykresli"); } public void smaz() { System.out.println("Kruh.smaz"); } }
Poznámky
public class Ctverec extends Tvar{ private int strana; public Ctverec() { strana = 12; } public double accept(Visitor v) { return v.visit(this); } public int getStrana() { return strana; } public void vykresli() { System.out.println("Ctverec.vykresli"); } public void smaz() { System.out.println("Ctverec.smaz"); } } public interface Visitor { public double visit(Kruh c); public double visit(Ctverec s); }
Poznámky
public class VisitObsah implements Visitor{ public double visit(Kruh c){ return Math.PI * c.getPolomer() * c.getPolomer(); } public double visit(Ctverec s){ return s.getStrana() * s.getStrana(); } } public class VisitObvod implements Visitor{ public double visit(Kruh c) { return 2 * Math.PI * c.getPolomer(); } public double visit(Ctverec s){ return 4 * s.getStrana(); } }
Poznámky
public class TvarTest { public static void main(String args[]) { VisitObsah visitObsah = new VisitObsah(); VisitObvod visitObvod = new VisitObvod(); List
tvary = new ArrayList(); tvary.add(new Ctverec()); tvary.add(new Kruh()); tvary.add(new Kruh()); tvary.add(new Ctverec()); tvary.add(new Kruh()); Iteratoritr = tvary.iterator(); while(itr.hasNext()) { Tvar s = itr.next(); s.vykresli(); s.smaz(); System.out.printf("Obsah obrazce: %s je: %.2f\n", s.getClass().getName(), s.accept(visitObsah)); System.out.printf("Obvod obrazce: %s je: %.2f\n", s.getClass().getName(), s.accept(visitObvod)); } } }
Poznámky
Ctverec.vykresli Ctverec.smaz Obsah obrazce: Ctverec je: 144,00 Obvod obrazce: Ctverec je: 48,00 Kruh.vykresli Kruh.smaz Obsah obrazce: Kruh je: 804,25 Obvod obrazce: Kruh je: 100,53 Kruh.vykresli Kruh.smaz Obsah obrazce: Kruh je: 804,25 Obvod obrazce: Kruh je: 100,53 Ctverec.vykresli Ctverec.smaz Obsah obrazce: Ctverec je: 144,00 Obvod obrazce: Ctverec je: 48,00 Kruh.vykresli Kruh.smaz Obsah obrazce: Kruh je: 804,25 Obvod obrazce: Kruh je: 100,53
Poznámky
Shrnutí vzoru Návštěvník • Je zřejmé, že můžeme zavést novou operaci (metodu) nad libovolnou strukturou, a to pouze dodáním nové třídy implementující rozhraní Visitor. • Problém ale nastane, pokud rozšíříme stávající neměnnou strukturu tříd o další třídu. • To však odporuje úvodnímu předpokladu, že struktura tříd je neměnná.
59