Polecenie
Cel
Polecenie jest behawioralnym wzorcem projektowym który zmienia żądanie w samodzielny obiekt zawierający wszystkie informacje o tym żądaniu. Taka transformacja pozwala na parametryzowanie metod przy użyciu różnych żądań. Oprócz tego umożliwia opóźnianie lub kolejkowanie wykonywania żądań oraz pozwala na cofanie operacji.
Problem
Wyobraź sobie, że pracujesz nad nowym edytorem tekstu. Tworzysz pasek narzędziowy z przyciskami wywołującymi różne działania edytora. Masz już elegancką klasę Przycisk
która może być używana zarówno do przycisków paska, a także jako ogólne przyciski w różnych oknach dialogowych.
Mimo, że wszystkie te przyciski wyglądają podobnie, to mają wywoływać różne działania. Gdzie więc umieścić kod obiektów obsługujących kliknięcia przycisków? Najprostszym rozwiązaniem jest stworzenie wielu podklas dla każdego przypadku użycia przycisku. Takie podklasy zawierałyby kod wykonywany po wciśnięciu przycisku.
Szybko zauważasz, że to podejście jest wadliwe. Powstanie wielka liczba podklas, co byłoby akceptowalne, gdybyśmy przy okazji nie ryzykowali popsucia kodu tych podklas przy każdej zmianie klasy bazowej Przycisk
. Kod twojego interfejsu użytkownika byłby zależny od zmiennego kodu logiki biznesowej.
Ale to jeszcze nie wszystko. Niektóre operacje, jak kopiowanie/wklejanie tekstu, powinny być dostępne z wielu miejsc: po kliknięciu na mały przycisk “Kopiuj” na pasku narzędziowym, wybraniu z menu kontekstowego, czy też po wciśnięciu skrótu klawiszowego Ctrl+C
.
Na początku, gdy nasza aplikacja posiadała tylko pasek narzędziowy, umieszczenie implementacji operacji w podklasach przycisku miało sens. Innymi słowy, trzymanie kodu służącego do kopiowaniu tekstu w obrębie podklasy PrzyciskKopiuj
było w porządku. Jednak po zaimplementowaniu menu kontekstowego, skrótów i innych — trzeba duplikować kod operacji w wielu klasach lub uczynić menu zależnymi od przycisków, co jest jeszcze gorszą opcją.
Rozwiązanie
Dobre projektowanie oprogramowania często bazuje na zasadzie separacji odpowiedzialności, co zwykle skutkuje podziałem aplikacji na warstwy. Najczęstszy przykład: warstwa graficznego interfejsu użytkownika i warstwa logiki biznesowej. Pierwsza jest odpowiedzialna za renderowanie pięknego obrazu na ekranie, przechwytywanie sygnałów na wejściu i wyświetlanie efektów pracy użytkownika i aplikacji. Jednak gdy chodzi o wykonywanie ważnych zadań, jak obliczanie trajektorii księżyca, lub generowanie rocznego bilansu, warstwa interfejsu użytkownika deleguje te zadania warstwie logiki biznesowej.
W kodzie wyglądałoby to na przykład tak: obiekt graficznego interfejsu użytkownika wywołuje metodę obiektu logiki biznesowej, przekazując jej jakieś argumenty. Proces ten zwykle można opisać jako przesłanie żądania przez jeden obiekt drugiemu obiektowi.
Według wzorca Polecenie, obiekty GUI nie powinny wysyłać żądań bezpośrednio. Zamiast tego należy wyekstrahować szczegóły żądania, takie jak obiekt docelowy, nazwę metody i listę argumentów do osobnej klasy polecenie posiadającej tylko jedną metodę — wywołującą to żądanie.
Obiekty polecenie stanowią łącza pomiędzy obiektami interfejsu użytkownika i logiki biznesowej. Od teraz, obiekt GUI nie musi wiedzieć który obiekt logiki biznesowej otrzyma żądanie i jak je obsłuży. Obiekt interfejsu użytkownika jedynie wywołuje polecenie, a ono samo zajmuje się szczegółami.
Kolejnym etapem jest zaimplementowanie wszystkim poleceniom jednakowego interfejsu. Zazwyczaj posiada on jedną tylko metodę wywołującą działanie, która nie przyjmuje parametrów. Taki interfejs pozwala jednemu nadawcy wywoływać wiele różnych poleceń bez konieczności sprzęgania go z konkretnymi klasami poleceń. Dodatkowo można teraz wymieniać obiekty-polecenia powiązane z nadawcą, a tym samym zmieniać jego zachowanie w trakcie działania programu.
Brakuje jeszcze jednego elementu układanki — parametrów żądania. Obiekt graficznego interfejsu użytkownika mógł dostarczyć obiektowi warstwy logiki biznesowej jakieś parametry. Skoro wykonanie polecenia nie przyjmuje żadnych parametrów, to jak przekazać odbiorcy szczegóły żądania? Otóż albo te dane powinny być wcześniej skonfigurowane w poleceniu, albo polecenie powinno móc pozyskać je samodzielnie.
Wróćmy do naszego edytora tekstu. Po zastosowaniu wzorca Polecenie, nie potrzebujemy tych wszystkich podklas przycisku, by zaimplementować różne reakcje na kliknięcie. Wystarczy umieścić w klasie bazowej Przycisk
jedno pole przechowujące odniesienie do obiektu typu polecenie i sprawić, by kliknięcie powodowało uruchomienie tego polecenia.
Należy zaimplementować kilka klas polecenie dla każdej możliwej operacji i połączyć je z konkretnymi przyciskami, zależnie od planowanej reakcji na wciskanie ich.
Inne elementy GUI, jak menu, skróty czy całe okna dialogowe można zaimplementować w taki sam sposób: powiązać je z poleceniem które będzie uruchamiane w odpowiedzi na interakcję użytkownika z danym elementem. Jak być może się już domyślasz, elementy związane z tymi samymi działaniami będą połączone z tymi samymi poleceniami, zapobiegając tym samym duplikacji kodu.
W rezultacie polecenia stają się poręczną warstwą pośrednią redukującą sprzężenie pomiędzy graficznym elementem użytkownika i warstwami logiki biznesowej. A to tylko część zysków płynących z użycia wzorca Polecenie!
Analogia do prawdziwego życia
Podczas długiego spaceru po mieście, docierasz do miłej restauracji i siadasz przy oknie. Przyjazny kelner szybko przyjmuje zamówienie, spisując je na małym kawałku papieru. Następnie kelner idzie do kuchni i przykleja kartkę na ścianie. Po jakimś czasie zamówienie dociera do szefa kuchni, który przygotowuje danie, a następnie umieszcza posiłek na tacce wraz z zamówieniem. Kelner znajduje tackę, sprawdza zgodność z zamówieniem i zanosi ją do stolika.
Zamówienie na papierze stanowi polecenie. Trafia do kolejki, do momentu aż szef kuchni je przygotuje. Zamówienie zawiera wszystkie niezbędne informacje wymagane do przygotowania posiłku. Umożliwia to kucharzowi rozpoczęcie gotowania od razu, zamiast ustalać szczegóły z klientem na własną rękę.
Struktura
-
Klasa Nadawca (lub wywołująca) jest odpowiedzialna za inicjowanie żądań. Musi ona zawierać pole przechowujące odniesienia do obiektu polecenia. Nadawca uruchamia polecenie zamiast przesyłać żądanie bezpośrednio do odbiorcy. Zauważ, że nadawca nie jest odpowiedzialny za tworzenie obiektu polecenie. Zazwyczaj otrzymuje wcześniej przygotowane polecenie od klienta za pośrednictwem konstruktora.
-
Interfejs Polecenie zwykle deklaruje pojedynczą metodę służącą wykonaniu polecenia.
-
Konkretne polecenia implementują różne rodzaje żądań. Konkretne polecenie nie powinno wykonywać pracy samodzielnie, lecz przekazać je do jednego z obiektów logiki biznesowej. Jednak dla uproszczenia kodu, klasy te można złączyć.
Parametry potrzebne do uruchomienia metody na obiekcie odbiorcy można zadeklarować w formie pól konkretnego polecenia. Obiekty poleceń można uczynić niezmienialnymi, zezwalając na inicjalizację tych pól wyłącznie za pośrednictwem konstruktora.
-
Klasa Odbiorca zawiera jakąś logikę biznesową. Prawie każdy obiekt może pełnić rolę odbiorcy. Większość poleceń obsługuje tylko szczegóły przekazania żądania do odbiorcy, zaś faktyczną pracę wykonuje ten ostatni.
-
Klient tworzy i konfiguruje konkretne obiekty żądań. Klient musi przekazać wszystkie parametry żądania, włącznie z instancją odbiorcy, do konstruktora polecenia. Następnie otrzymane polecenie można skojarzyć z jednym lub wieloma nadawcami.
Pseudokod
W poniższym przykładzie, wzorzec Polecenie pozwala śledzić historię wykonanych działań i umożliwia cofnięcie danej operacji jeśli zaistnieje potrzeba.
Polecenia skutkujące zmianą stanu edytora (na przykład wycinanie i wklejanie tekstu) wykonują kopię stanu edytora zanim wywołają działanie skojarzone z tym poleceniem. Po wykonaniu polecenia jest ono umieszczane w historii poleceń (stos obiektów polecenie) wraz z kopią zapasową stanu edytora na tamten moment. Jeśli użytkownik zechce cofnąć jakieś działanie, aplikacja może pobrać ostatnie polecenie z historii, odczytać skojarzoną z nim kopię zapasową stanu edytora i przywrócić ją.
Kod klienta (elementy GUI, historia poleceń, itd.) nie jest sprzężony z konkretnymi klasami poleceń ponieważ współpracuje z poleceniami za pośrednictwem interfejsu polecenia. Takie podejście pozwala wdrożyć nowe polecenia do aplikacji bez psucia istniejącego kodu.
Zastosowanie
Zastosuj wzorzec Polecenie gdy chcesz parametryzować obiekty za pomocą działań.
Wzorzec Polecenie pozwala przekształcić wywołanie metody w samodzielny obiekt. Zmiana taka otwiera wiele ciekawych zastosowań: można przekazywać polecenia jako argumenty metody, przechowywać je w innych obiektach, zamieniać powiązane polecenia w trakcie działania programu, itp.
Oto przykład: pracujesz nad komponentem graficznego interfejsu użytkownika takim jak menu kontekstowe i chcesz aby użytkownicy mogli konfigurować elementy menu odpowiadające działaniom.
Wzorzec Polecenie pozwala układać kolejki zadań, ustalać harmonogram ich wykonania bądź uruchamiać je zdalnie.
Jak każdy inny obiekt, polecenie można serializować, co oznacza przekształcenie go w łańcuch znaków dający się łatwo zapisać w pliku lub bazie danych. Można później taki łańcuch znaków przywrócić do formy pierwotnego obiektu polecenia. Dzięki temu można opóźniać i ustalać harmonogram wykonywania poleceń. Co więcej, w taki sam sposób można kolejkować, notować w dzienniku lub wysyłać polecenia przez sieć.
Stosuj wzorzec Polecenie gdy chcesz zaimplementować operacje odwracalne.
Chociaż istnieje wiele sposobów na implementację funkcjonalności cofnij/ponów, wzorzec Polecenie jest prawdopodobnie najpopularniejszym.
Aby móc wycofywać działania, trzeba zaimplementować historię wykonanych działań. Historia poleceń jest stosem zawierającym wszystkie obiekty wykonanych poleceń wraz ze skojarzonymi z nimi kopiami zapasowymi stanu aplikacji.
Ta metoda ma dwie wady. Po pierwsze, zapisanie stanu aplikacji może nie być tak proste, gdyż część jej danych może być prywatna. Problem ten można obejść stosując wzorzec Pamiątka.
Po drugie, kopie zapasowe stanów mogą zużywać sporo pamięci RAM. Dlatego czasem można uciec się do alternatywnej implementacji: zamiast przywracać przeszły stan, można wykonać polecenie odwrotne. Takie polecenie również jednak miałoby swoją cenę: może okazać się trudne lub wręcz niemożliwe do zaimplementowania.
Jak zaimplementować
-
Zadeklaruj interfejs polecenia z pojedynczą metodą uruchamiającą.
-
Dokonaj ekstrakcji żądań do konkretnych, odrębnych klas poleceń które implementują interfejs polecenia. Każda klasa powinna mieć zestaw pól służących przechowywaniu argumentów żądania wraz z odniesieniem do faktycznego obiektu odbiorcy. Wszystkie te wartości muszą być inicjalizowane za pośrednictwem konstruktora polecenia.
-
Zidentyfikuj klasy które będą pełnić rolę nadawców. Dodaj tym klasom pola służące przechowywaniu poleceń. Nadawcy powinni komunikować się z poleceniami wyłącznie za pośrednictwem interfejsu polecenia. Nadawcy na ogół nie tworzą obiektów polecenie sami, lecz otrzymują je od strony kodu klienta.
-
Zmień nadawców w taki sposób, aby uruchamiali polecenie zamiast wysyłania żądania bezpośrednio do odbiorcy.
-
Klient powinien inicjalizować obiekty w następującej kolejności:
- Tworzyć odbiorców.
- Tworzyć polecenia i kojarzyć je z odpowiednimi odbiorcami, jeśli istnieje potrzeba,
- Tworzyć nadawców i kojarzyć ich z konkretnymi poleceniami.
Zalety i wady
- Zasada pojedynczej odpowiedzialności. Można rozprzęgnąć klasy wywołujące polecenia od klas faktycznie je wykonujących.
- Zasada otwarte/zamknięte. Można wprowadzić nowe polecenia do aplikacji bez psucia istniejącego kodu klienta.
- Pozwala zaimplementować cofnij/ponów.
- Pozwala zaimplementować opóźnione wykonywanie działań.
- Można złożyć zestaw prostszych poleceń w jedno skomplikowane.
- Kod może stać się bardziej skomplikowany gdyż wprowadzamy całą nową warstwę pomiędzy nadawcami a odbiorcami.
Powiązania z innymi wzorcami
-
Wzorce Łańcuch zobowiązań, Polecenie, Mediator i Obserwator dotyczą różnych sposobów na łączenie nadawców z odbiorcami żądań:
- Łańcuch zobowiązań przekazuje żądanie sekwencyjnie wzdłuż dynamicznego łańcucha potencjalnych odbiorców, aż któryś z nich je obsłuży.
- Polecenie pozwala nawiązywać jednokierunkowe połączenia pomiędzy nadawcami i odbiorcami.
- Mediator eliminuje bezpośrednie połączenia pomiędzy nadawcami a odbiorcami, zmuszając ich do komunikacji za pośrednictwem obiektu mediator.
- Obserwator pozwala odbiorcom dynamicznie zasubskrybować się i zrezygnować z subskrypcji żądań.
-
Obsługujący w Łańcuchu zobowiązań mogą być zaimplementowani jako Polecenia. Można wówczas wykonać wiele różnych działań reprezentowanych jako żądania na tym samym obiekcie-kontekście.
Istnieje jednak jeszcze jedno podejście, według którego samo żądanie jest obiektem Polecenie. W takim przypadku możesz wykonać to samo działanie na łańcuchu różnych kontekstów.
-
Można stosować Polecenie i Pamiątkę jednocześnie — implementując funkcjonalność “cofnij”. W takim przypadku, polecenia są odpowiedzialne za wykonywanie różnych działań na obiekcie docelowym, zaś pamiątki służą zapamiętaniu stanu obiektu tuż przed wykonaniem polecenia.
-
Polecenie i Strategia mogą wydawać się podobne, ponieważ oba mogą służyć parametryzacji obiektu jakimś działaniem. Mają jednak inne cele.
-
Za pomocą Polecenia można konwertować dowolne działanie na obiekt. Parametry działania stają się polami tego obiektu. Konwersja zaś pozwala odroczyć wykonanie działania, kolejkować je i przechowywać historię wykonanych działań, a także wysyłać polecenia zdalnym usługom, itd.
-
Z drugiej strony, Strategia zazwyczaj opisuje różne sposoby wykonywania danej czynności, pozwalając zamieniać algorytmy w ramach jednej klasy kontekstu.
-
-
Prototyp może pomóc stworzyć historię, zapisując kopie Poleceń.
-
Wzorzec Odwiedzający można traktować jak potężniejszą wersję Polecenia. Jego obiekty mogą wykonywać różne polecenia na obiektach różnych klas.