Zdjęcie z gry Commandos 2
Wzorzec Strategy polega na stworzeniu interfejsu implementowanego przez klasy, które zajmują się tym samym ale w inny sposób. Np. gdy chcemy pobrać listy użytkowników z różnych stron, z różnym API. Ale, ale! Po co ja to powtarzam skoro Strategia była omawiana na samym początku serii, a dzisiaj zajmujemy się Poleceniem?
Czytając o wzorcu Command, miałem podobne przemyślenia jak zajmując się Abstract Factory. Deja vu. Początek wydawał się bowiem zupełnie taki jak we wspomnianym Strategy. Wszystkie trzy wzorce wymagają albo interfejsu, albo chociaż współdzielenia klasy dziedziczonej. Różnice tkwią w szczegółach oraz w przeznaczeniu każdego wzorca:
- Strategii powinniśmy użyć, kiedy mamy kilka algorytmów do osiągnięcia tego samego rezultatu (wybieramy jeden, najlepszy w danej sytuacji)
- Factory najlepiej sprawdza się przy masowym tworzeniu podobnych obiektów (wybieramy szablon produkcji i uruchamiamy fabrykę)
- Command, trochę jak Strategy, też oferuje kilka algorytmów, ale każdy służy do czegoś innego, możemy je zakolejkować, a nawet cofnąć
Mówiąc krótko:
Wzorzec Polecenie to ulepszony wzorzec Strategia.
Polecenie – hermetyzuje żądania w postaci obiektów, co umożliwia paramateryzowanie różnych obiektów zróżnicowanymi żądaniami (takimi jak np. żądania kolejkowania lub rejestracji) oraz obsługiwania operacji, które można wycofać.
Rusz głową! Wzorce projektowe
Wyobraźcie sobie, że macie jakiś obiekt. Niech to będzie odpowiedzialny np. za odtwarzanie plików wideo: „Player”. Jak myślicie – ile powinien mieć metod aby spełniał swoją funkcję?
Pomyślmy: odtwórz plik, zatrzymaj, wznów, podgłośnij, ścisz, przewiń, cofnij, wyjdź… Póki co wyszło nam już osiem, a to przecież tylko niezbędne minimum. Aby wyróżnić się na tle innych podobnych aplikacji trzeba dodać kontrolę jasności, szybkości odtwarzania, zoom i masę innych. Tymczasem klasa obiektu rośnie i się komplikuje.

A jeśli każda z tych metod byłaby odrębną klasą? Np. mamy klasę Ścisz, która ma tylko jedną metodę execute() która ścisza audio i nic więcej nie robi. Klasa musi oczywiście implementować odpowiedni interfejs (czyli Command) podobnie jak wszystkie inne klasy-polecenia. Wtedy wystarczy, że przed skorzystaniem ze ściszenia w Playerze, wyślemy temu obiektowi odpowiednie polecenie (setCommand()) które ustawia tę jedyną metodę klasy Player na wywoływanie execute() obiektu Ścisz.
Z jednej strony wydaje się to fajną sztuczką zapewniającą wysoką hermetyzację, ale z drugiej wygląda na prosty przepis aby utonąć w ilości tworzonych klas-komandosów (stąd grafika tytułowa, heh). Rozwiązanie jest owszem, fajne – ale tak jak z Singletonem, należy go używać z umiarem – wtedy kiedy rzeczywiście rozwiązuje jakiś problem.
Ale po kolei…
Parę odcinków temu, przy okazji omawiania wzorca Observer, tłumaczyłem wam zasadę działania znanego z DnD czaru Warunkowanie. Nie będę się powtarzał, ważne jest jednak to, że w Warunkowaniu umieszczaliśmy inny czar, który aktywował się w wybranej wcześniej sytuacji, na wybranym wcześniej celu.
Z jednej strony wygoda bo pełna automatyka, ale z drugiej – trochę nas to ograniczało, bo po inicjalizacji traciliśmy kontrolę nad zawartym zaklęciem. A co jeśli moglibyśmy sami wybrać czas i miejsce aktywacji?
Powiecie: głupota! Przecież tak właśnie działają zaklęcia bez żadnych warunkowań! Wybieram kulę ognia i strzelam w bandę goblinów dopiero gdy atakują kupą, wybieram kamienną skórę i rzucam na siebie dopiero kiedy dochodzi do walki w zwarciu.
A gdyby do takiego nie-automatycznego warunkowania można by włożyć więcej niż jeden czar?
O! I tu pojawia się potencjał! Jeśli mamy do czynienia z potężnym przeciwnikiem, to często już na starcie jest chroniony magią ochronną. Kamienna skóra blokuje obrażenia fizyczne, Ochrona przed żywiołami sprawia, że kula ognia też nie zada żadnych obrażeń. A Przyśpieszenie sprawi, że sam będzie atakował dwa razy szybciej.
W takich sytuacjach w Baldurze najlepiej sprawdzały się tzw. Sekwencery czyli właśnie ulepszone Warunkowania, zdolne zakolejkować i odpalić szybko jeden po drugim aż trzy zaklęcia. Moglibyśmy tam zatem umieścić kolejno Wyłom, Wrażliwość i Spowolnienie, aby usunąć z wroga wszelkie buffy.
Ale jak coś takiego wyglądałoby w kodzie (pełna wersja tutaj) wykorzystującym wzorzec Polecenie?
Na starcie potrzebujemy czarodzieja, któremu wystarczy jedna metoda.
static class Wizard{
Commands currentCommand;
void castSpell(Enemy enemy){
System.out.print("Czarodziej rzuca czar: ");
currentCommand.engage(enemy);
}
}
Do tego interfejs implementowany przez różne polecenia (będące tutaj zaklęciami):
interface Commands{
void engage(Enemy enemy);
}
static class FireBall implements Commands{
@Override
public void engage(Enemy enemy) {
System.out.println("Kula ognia.");
enemy.hp -= 33;
}
I już możemy zaobserwować efekty:

Ale to nie wszystko, bo wcześniej wspomniałem:
„Command, podobnie jak Strategy, też oferuje kilka algorytmów, ale każdy służy do czegoś innego, możemy je zakolejkować, a nawet cofnąć.”
Cofnąć. Wydaje się trudne, a wystarczy do interfejsu Commands dodać drugą metodę, np. revert() która będzie odwrotnością tej pierwszej. Zatem jeśli Kula ognia zadaje 33 punkty obrażeń, to revert ma tyle samo przywracać:
public void revert() {
enemy.hp += 33;
}
Co fajne, kolejne polecenia możemy zakolejkować np. dodając je do stosu (Stack<>), a następnie wycofać wszystkie jednym ruchem, co w naszym wypadku byłoby swoistym wczytaniem stanu gry (loadGame()):

Rzeczona kolejka prowadzi nas też wprost do Sekwencera – zamiast wywoływać castSpell() możemy wrzucić wszystkie zaklęcia na stos (addToSequencer) i odpalić je razem metodą fireSequencer().
void fireSequencer(){
System.out.println("Odpalono sekwencer zaklęć:");
for (Commands command : allCommands) {
command.engage(currentEnemy);
}
}

ITCandidateEvaluator
Teraz standardowe pytanie – czy widzę użycie wzorca Command w moim kolejnym projekcie?
Jeszcze nie, ale to się może zmienić.
Wydaje mi się, że Polecenie może wyjść w praniu przy refaktoryzacji kodu gdzieś pod koniec prac nad aplikacją. Np. jeśli zobaczę potrzebę umieszczenia przycisku „Cofnij” usuwającego ostatnio wprowadzoną zmianę. Albo gdy będę chciał hermetyzować niektóre metody danej klasy, tak by łatwo było mi je zmienić lub dodać kolejne w przyszłości (reguła: Otwarty/Zamknięty).
Samo kolejkowanie wprowadzonych poleceń też może się przydać w zakresie logowania informacji oraz błędów.
Na dzisiaj tyle i jak zwykle dziękuję za uwagę. Widzimy się już niedługo przy wzorcach Adaper oraz Fasada.