Wzorce projektowe – Polecenie
Polecenie, inaczej nazywany komendą (command), należy do rodziny czynnościowych wzorców projektowych, którego zadaniem jest agregacja i hermetyzacja żądań do wykonania. Żądania te traktowane są jako osobne obiekty, które mogą być parametryzowane w zależności od rodzaju odbiorcy.
Charakterystyka wzorca polecenie
Poniżej przedstawiony jest diagram UML, pokazujący strukturę wzorca.
Patrząc na diagram UML wzorca polecenia, możemy wyróżnić kilka elementów:
- polecenie – interfejs definiujący metody jakie każde konkretne polecenie musi wykonać, zazwyczaj są to metody typu wykonaj (execute), cofnij (undo), ustaw parametr (setParameter), itp.
- konkretne polecenie – klasa, implementująca interfejs polecenia; dla poszczególnych komend definiujemy osobną klasę
- odbiorca (receiver) – jest przedmiotem akcji wykonywanej przez polecenie, komunikuje się z konkretnym poleceniem, przekazując żądania klienta
- nadawca (invoker) – ustala odbiorcę żądania, wywołuje metody polecenia
- klient (client) – nasza aplikacja, tworzy konkretne polecenie, ustawia odbiorcę i używa nadawcę
Przykład zastosowania wzorca polecenie – obsługa urządzenia elektronicznego
Teoria może wyglądać dosyć skomplikowanie, dlatego najłatwiej zrozumieć działanie tego wzorca na konkretnym przykładzie. Przykładowa aplikacja będzie miała zadanie utworzenie urządzenia elektronicznego(np. telewizora lub radia), a następnie utworzenie i wykorzystanie poleceń sterujących tymi odbiornikami.
W przełożeniu na powyższy diagram i jego opis:
- poleceniami będą: włącz (turn on), wyłącz (turn off), pogłośnij (turn volume up), ścisz (turn volume down)
- odbiorcą będzie telewizor lub radio
- nadawcą będzie pilot (remote controller)
Zaczynamy od utworzenia interfejsu dla naszych urządzeń elektrycznych, który deklaruje akcje wykonywane przez konkretne urządzania, tj. włączanie, wyłącznie urządzania lub zmiana głośności.
1 2 3 4 5 6 7 8 9 10 |
namespace DesignPatterns\Command\ElectronicDevice; interface ElectronicDeviceInterface { public function on(): void; public function off(): void; public function volumeUp(): void; public function volumeDown(): void; public function isOn(): bool; } |
Dalej piszemy klasy konkretnych urządzeń implementujące interfejs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
namespace DesignPatterns\Command\ElectronicDevice; class Television implements ElectronicDeviceInterface { private $state = 'off'; private $volume = 10; public function on(): void { $this->state = 'on'; echo 'TV is on'; } public function off(): void { $this->state = 'off'; echo 'TV is off'; } public function volumeUp(): void { if (!$this->isOn() || $this->volume >= 30) { return; } $this->volume++; echo 'TV volume is at '.$this->volume; } public function volumeDown(): void { if (!$this->isOn() || $this->volume <= 0) { return; } $this->volume--; echo 'TV volume is at '.$this->volume; } public function isOn(): bool { return $this->state === 'on'; } } |
Kolejnym elementem będzie utworzenie poleceń wywoływanych na rzecz odbiorcy, czyli klasy telewizora. Zacznijmy od interfejsu polecenia.
1 2 3 4 5 6 7 |
namespace DesignPatterns\Command\ElectronicDevice\Command; interface CommandInterface { public function execute(): void; public function undo(): void; } |
Jak widzimy, nasze polecenia oprócz metody execute (wykonaj), będzie również mogły cofać wykonanie polecenia za pomocą metody undo.
Tworzymy klasy dla poszczególnych poleceń. Polecenie w konstruktorze będzie otrzymywał obiekt odbiorcy (czyli w tym przypadku urządzenie elektryczne) na rzecz którego polecenie będzie wykonywane.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace DesignPatterns\Command\ElectronicDevice\Command; use DesignPatterns\Command\ElectronicDevice\ElectronicDeviceInterface; class TurnOn implements CommandInterface { private $electronicDevice; public function __construct(ElectronicDeviceInterface $electronicDevice) { $this->electronicDevice = $electronicDevice; } public function execute(): void { $this->electronicDevice->on(); } public function undo(): void { $this->electronicDevice->off(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace DesignPatterns\Command\ElectronicDevice\Command; use DesignPatterns\Command\ElectronicDevice\ElectronicDeviceInterface; class TurnOff implements CommandInterface { private $electronicDevice; public function __construct(ElectronicDeviceInterface $electronicDevice) { $this->electronicDevice = $electronicDevice; } public function execute(): void { $this->electronicDevice->off(); } public function undo(): void { $this->electronicDevice->on(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace DesignPatterns\Command\ElectronicDevice\Command; use DesignPatterns\Command\ElectronicDevice\ElectronicDeviceInterface; class TurnVolumeUp implements CommandInterface { private $electronicDevice; public function __construct(ElectronicDeviceInterface $electronicDevice) { $this->electronicDevice = $electronicDevice; } public function execute(): void { $this->electronicDevice->volumeUp(); } public function undo(): void { $this->electronicDevice->volumeDown(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace DesignPatterns\Command\ElectronicDevice\Command; use DesignPatterns\Command\ElectronicDevice\ElectronicDeviceInterface; class TurnVolumeDown implements CommandInterface { private $electronicDevice; public function __construct(ElectronicDeviceInterface $electronicDevice) { $this->electronicDevice = $electronicDevice; } public function execute(): void { $this->electronicDevice->volumeDown(); } public function undo(): void { $this->electronicDevice->volumeUp(); } } |
Ostatnim elementem jest nadawca (w naszej aplikacji będzie to pilot – remote controller), którego zadaniem będzie wykonywanie przekazanych poleceń.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace DesignPatterns\Command\ElectronicDevice; use DesignPatterns\Command\ElectronicDevice\Command\CommandInterface; class RemoteController { public function press(CommandInterface $command): void { $command->execute(); } public function pressUndo(CommandInterface $command): void { $command->undo(); } } |
Teraz łączymy wszystko w naszej aplikacji.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
use DesignPatterns\Command\ElectronicDevice\Command\TurnOff; use DesignPatterns\Command\ElectronicDevice\Command\TurnOn; use DesignPatterns\Command\ElectronicDevice\Command\TurnVolumeDown; use DesignPatterns\Command\ElectronicDevice\Command\TurnVolumeUp; use DesignPatterns\Command\ElectronicDevice\RemoteController; use DesignPatterns\Command\ElectronicDevice\Television; // Create electronic device $tv = new Television(); // Create commands $turnOnCommand = new TurnOn($tv); $turnOffCommand = new TurnOff($tv); $turnVolumeUpCommand = new TurnVolumeUp($tv); $turnVolumeDownCommand = new TurnVolumeDown($tv); // Exectue commands $remoteController = new RemoteController(); $remoteController->press($turnOnCommand); $remoteController->press($turnOffCommand); $remoteController->pressUndo($turnOffCommand); $remoteController->press($turnVolumeUpCommand); $remoteController->press($turnVolumeUpCommand); $remoteController->press($turnVolumeUpCommand); $remoteController->press($turnVolumeDownCommand); $remoteController->press($turnVolumeDownCommand); $remoteController->press($turnVolumeDownCommand); |
W pierwszej kolejności aplikacja (klient) tworzy konkretne urządzenie elektryczne, a następnie definiuje polecenia, które chcemy wykonać. Następnie tworzona jest instancja pilota, która steruje wykonywaniem poszczególnych poleceń na rzecz urządzenia.
Wynikiem działania aplikacji jest:
1 2 3 4 5 6 7 8 9 |
TV is on TV is off TV is on TV volume is at 11 TV volume is at 12 TV volume is at 13 TV volume is at 12 TV volume is at 11 TV volume is at 10 |
Powyższy przykład można zobaczyć i pobrać na GitHub: https://github.com/molitorys/design-patterns/tree/master/src/Command/ElectronicDevice
Podsumowanie
Na pierwszy rzut oka, zastosowanie wzorca polecenia może wydawać się niepotrzebną komplikacją. I faktycznie, wzorzec ten wymusza tworzenie wielu obiektów i wprowadza kilka nowych elementów, które mogą wydawać się zbędne. Osobiście, dla mniejszych projektów nie zawracałbym sobie specjalnie głowy tym wzorcem, natomiast dla większych projektów może być bardzo pomocny. Zastosowanie poleceń pozwala na:
- hermetyzacje żądań,
- umożliwia oddzielenie kodu wysyłania (invoker) i odbierania (receiver) żądań,
- pozwala na monitorowanie wykonywania żądań,
- pozwala na cofanie poleceń,
- umożliwia manipulowanie poleceniami i łatwe dodawanie nowych,
- daje nam bardziej elastyczny i ładniejszy kod.