Wiosenna WYPRZEDAŻ

Polecenie

Znany też jako: Action, Transaction, Command

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.

Wzorzec projektowy Polecenie

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.

Rozwiązanie problemu za pomocą wzorca Polecenie

Wszystkie przyciski w aplikacji wywodzą się z tej samej klasy.

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.

Mnóstwo podklas przycisku

Mnóstwo podklas przycisku. Co może pójść nie tak?

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.

Wiele klas implementuje tę samą funkcjonalność

Wiele klas implementuje tę samą funkcjonalność.

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.

Warstwa graficznego interfejsu użytkownika może mieć bezpośredni dostęp do warstwy logiki biznesowej

Obiekty graficznego interfejsu użytkownika mogą mieć bezpośredni dostęp do obiektów warstwy logiki biznesowej.

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.

Dostęp do warstwy logiki biznesowej za pośrednictwem polecenia.

Dostęp do warstwy logiki biznesowej za pośrednictwem polecenia.

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.

Obiekty graficznego interfejsu użytkownika delegują pracę poleceniom

Obiekty graficznego interfejsu użytkownika delegują pracę poleceniom.

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

Składanie zamówienia w restauracji

Składanie zamówienia w restauracji.

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

Struktura wzorca projektowego PolecenieStruktura wzorca projektowego Polecenie
  1. 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.

  2. Interfejs Polecenie zwykle deklaruje pojedynczą metodę służącą wykonaniu polecenia.

  3. 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.

  4. 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.

  5. 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.

Struktura przykładu użycia wzorca Polecenie

Odwracalne działania w edytorze tekstu.

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.

// Bazowa klasa polecenie definiuje wspólny interfejs wszystkich
// konkretnych poleceń.
abstract class Command is
    protected field app: Application
    protected field editor: Editor
    protected field backup: text

    constructor Command(app: Application, editor: Editor) is
        this.app = app
        this.editor = editor

    // Zrób kopię zapasową stanu edytora.
    method saveBackup() is
        backup = editor.text

    // Przywróć stan edytora.
    method undo() is
        editor.text = backup

    // Metoda wykonująca zadeklarowana jest jako abstrakcyjna,
    // aby zmusić wszystkie konkretne polecenia do
    // zaimplementowania jej we własnym zakresie. Metoda musi
    // zwracać prawdę lub fałsz zależnie od tego, czy polecenie
    // zmienia stan edytora lub pozostawia stan bez zmian.
    abstract method execute()


// Tu umieszczane są konkretne polecenia.
class CopyCommand extends Command is
    // Polecenie kopiuj nie jest zapisywane w historii ponieważ
    // nie zmienia stanu edytora.
    method execute() is
        app.clipboard = editor.getSelection()
        return false

class CutCommand extends Command is
    // Polecenie wytnij zmienia stan edytora, dlatego trzeba
    // zapisać stan w historii. Stan będzie zapisywany zawsze
    // gdy metoda zwróci prawdę.
    method execute() is
        saveBackup()
        app.clipboard = editor.getSelection()
        editor.deleteSelection()
        return true

class PasteCommand extends Command is
    method execute() is
        saveBackup()
        editor.replaceSelection(app.clipboard)
        return true

// Funkcja cofania operacji również jest poleceniem.
class UndoCommand extends Command is
    method execute() is
        app.undo()
        return false


// Globalna historia poleceń jest po prostu stosem.
class CommandHistory is
    private field history: array of Command

    // Ostatni na wejściu...
    method push(c: Command) is
        // Wepchnij polecenie na koniec tablicy historii.

    // ...pierwszy na wyjściu.
    method pop():Command is
        // Pobierz najświeższe polecenie z historii.


// Klasa edytora posiada narzędzia do edycji tekstu. Pełni rolę
// odbiorcy: wszystkie polecenia delegują faktyczne wykonanie
// metodom edytora.
class Editor is
    field text: string

    method getSelection() is
        // Zwróć zaznaczony tekst.

    method deleteSelection() is
        // Skasuj zaznaczony tekst.

    method replaceSelection(text) is
        // Wstaw zawartość schowka w bieżącym miejscu.


// Klasa aplikacji ustanawia relacje między obiektami. Pełni
// rolę nadawcy: gdy trzeba coś zrobić, tworzy obiekt polecenia
// i uruchamia go.
class Application is
    field clipboard: string
    field editors: array of Editors
    field activeEditor: Editor
    field history: CommandHistory

    // Kod przypisujący polecenia do obiektów interfejsu
    // użytkownika może wyglądać w sposób następujący.
    method createUI() is
        // ...
        copy = function() { executeCommand(
            new CopyCommand(this, activeEditor)) }
        copyButton.setCommand(copy)
        shortcuts.onKeyPress("Ctrl+C", copy)

        cut = function() { executeCommand(
            new CutCommand(this, activeEditor)) }
        cutButton.setCommand(cut)
        shortcuts.onKeyPress("Ctrl+X", cut)

        paste = function() { executeCommand(
            new PasteCommand(this, activeEditor)) }
        pasteButton.setCommand(paste)
        shortcuts.onKeyPress("Ctrl+V", paste)

        undo = function() { executeCommand(
            new UndoCommand(this, activeEditor)) }
        undoButton.setCommand(undo)
        shortcuts.onKeyPress("Ctrl+Z", undo)

    // Uruchom polecenie i sprawdź czy trzeba je zapisać w
    // historii.
    method executeCommand(command) is
        if (command.execute())
            history.push(command)

    // Weź najświeższe polecenie z historii i uruchom jego
    // metodę wycofującą. Zwróć uwagę, że nie znamy klasy tego
    // polecenia i nie musimy znać, bo polecenie samo wie jak
    // cofnąć rezultat swojego działania.
    method undo() is
        command = history.pop()
        if (command != null)
            command.undo()

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ć

  1. Zadeklaruj interfejs polecenia z pojedynczą metodą uruchamiającą.

  2. 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.

  3. 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.

  4. Zmień nadawców w taki sposób, aby uruchamiali polecenie zamiast wysyłania żądania bezpośrednio do odbiorcy.

  5. 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.

Przykłady kodu

Polecenie w języku C# Polecenie w języku C++ Polecenie w języku Go Polecenie w języku Java Polecenie w języku PHP Polecenie w języku Python Polecenie w języku Ruby Polecenie w języku Rust Polecenie w języku Swift Polecenie w języku TypeScript