Wzorzec projektowy Polecenie (komenda, ang. Command)

Cel

Hermetyzacja poleceń do wykonania w postaci obiektów, umożliwienie parametryzacji klientów obiektami poleceń, wsparcie dla poleceń odwracalnych

Struktura

Podstawowym elementem wzorca jest interfejs Command, deklarujący metodę execute(). Jest to polimorficzna metoda reprezentująca polecenie do wykonania. Metoda ta jest implementowana w klasach ConcreteCommand w postaci polecenia wykonania określonej akcji na obiekcie-przedmiocie Receiver. Klient nie jest bezpośrednio związany ani z obiektem Command, ani z obiektem inicjującym jego wywołanie, czyli Invoker. Widzi jedynie odbiorcę wyników operacji ? obiekt Receiver.

Interakcje

Szczegółowy przepływ sterowania przedstawia diagram sekwencji. Inicjatorem przetwarzania jest obiekt Invoker, który zarządza obiektami typu Command. W momencie nadejścia żądania wykonania określonej operacji Invoker parametryzuje skojarzony z nią obiekt Command właściwym odbiorcą ich działań, czyli obiektem Receiver. Następnie wywołuje metodę execute() w tym obiekcie, powodując określone skutki w obiekcie Receiver, widoczne dla Klienta.

Uczestnicy

  • Command - definiuje interfejs obiektu reprezentującego polecenie
  • Concrete Command - jest powiązany z właściwym obiektem Receiver, implementuje akcję w postaci metody execute()
  • Client - tworzy Concrete Command
  • Invoker - ustala odbiorcę akcji każdego obiektu Command, wywołuje metodę execute()
  • Receiver - jest przedmiotem akcji wykonanej przez Command

W aplikacji okienkowej polecenia znajdujące się w menu są zdefiniowane w postaci obiektów typu Command. Każde polecenie jest inną implementacją tego interfejsu, i posiada innego odbiorcę, ustalanego w momencie wykonywania akcji (np. polecenie zamknięcia okna działa na aktualnie aktywne okno). W momencie kliknięcia na wybranej pozycji menu (czyli obiektu Invoker), wykonuje ona metodę execute() skojarzonego z nią polecenia typu Command, ustalając jego odbiorcę. Efekt, w postaci np. zamknięcia okna, jest widoczny dla klienta.

Konsekwencje

  • Usunięcie powiązania między nadawcą i przedmiotem polecenia
  • Łatwe dodawanie kolejnych obiektów Command
  • Możliwość manipulacji obiektami Command - polecenia złożone: wzorzec Composite
  • Polecenia mogą być odwracalne - zapamiętanie stanu przez Concrete Command, wykorzystanie wzorca Memento

Istotną korzyścią płynącą z zastosowania wzorca jest rozdzielenie zależności pomiędzy nadawcą (Klientem) i odbiorcą (obiektem Receiver) komunikatu. Zastosowanie polimorfizmu pozwala traktować poszczególne polecenia abstrakcyjnie, a co za tym idzie - dodawać nowe typy poleceń bez konieczności zmiany struktury systemu. Poszczególne obiekty Command mogą być dowolnie złożone, także w postaci kompozytów innych poleceń. Dodatkową zaletą użycia obiektu do hermetyzacji poleceń jest możliwość utworzenia w typie Command przeciwstawnej metody, która odwraca efekt wykonania polecenia. W takiej sytuacji obiekt ConcreteCommand musi zapamiętać stan obiektu Receiver sprzed wykonania operacji lub np. skorzystać z wzorca Memento.

Przykład

Bank zarządza grupą obiektów Account reprezentujących rachunki bankowe. Operacje bankowe, wykonywane na rachunkach, są implementacjami interfejsu Operation, posiadającego metodę execute(). Jej implementacja zależy od rodzaju operacji, dlatego w przypadku obiektu InterestChange będzie ona zmieniała stopę procentową, a w przypadku obiektu Transfer - dokonywała przelewu. Ponieważ każda operacja wymaga innych parametrów, dlatego są one przekazywane w konstruktorze poszczególnej klasy, a nie bezpośrednio w metodzie execute(). W tym przykładzie rolę obiektu Invoker pełni bank, ponieważ on wykonuje metodę execute(), a rolę przedmiotu polecenia (obiektu Receiver) - obiekt Account.

public class Bank { // Invoker, Client
  public void income(Account acc, long amount) {
    Operation oper = new Income(amount);
    acc.doOperation(oper);
  }
  public void transfer(Account from, Account to, long amount){
    Operation oper = new Transfer(to, amount);
    from.doOperation(oper);
  }
}
public class Account { // Reciever
  long balance = 0;
  Interest interest = new InterestA();
  History history = new History();

  public void doOperation(Operation oper) {
    oper.execute(this);
    history.log(oper);
  }
}

Jest to przykładowa implementacja klasy Bank, która pełni role Invoker i Client, oraz klasy Account, będącej odbiorcą poleceń. Klasa Bank definiuje metodę income(), która służy do wykonywania wpłaty na określony rachunek. W tym celu tworzy on instancję odpowiedniej operacji (klasy Income), a następnie przekazuje jej wykonanie obiektowi Account. Klasa Account wykonuje dowolną abstrakcyjną operację przekazaną z zewnątrz, np. przez klasę Bank. Dzięki temu dodanie nowej operacji bankowej nie powoduje konieczności jakiejkolwiek zmiany w klasie Account.

abstract public class Operation { // Command
  public void execute();
}
public class Income { // ConcreteCommand1
  public Income(long amount) {
  // store parameters...
  }
  public void execute(Account acc) {
    acc.add(amount);
  }
}
public class Transfer { // ConcreteCommand2
  public Income(Account to, long amount) {
    // store parameters...
  }
  public void execute(Account from) {
    from.subtract(amount);
    to.add(amount);
  }
}

Klasa Operation pełni rolę obiektu Command we wzorcu i definiuje abstrakcyjną metodę execute(). Jest ona pokrywana w klasach reprezentujących poszczególne operacje bankowe, które implementują ją zgodnie ze specyfiką wykonywanej operacji.

Zastosowania wzorca polecenia

Wielopoziomowe cofanie zmian. Jeżeli wszystkie działania użytkownika programu są zapisane jako obiekty poleceń, program może przechowywać na stosie wszystkie ostatnio wykonane działania. Jeżeli użytkownik chce cofnąć polecenie, program po prostu ściąga ostatnie polecenie ze stosu i uruchamia jego metodę cofnij().
Transakcje. Potrzebne jest też czasem automatyczne cofanie zmian jeżeli działanie operacji przerwie się w połowie, na przykład w instalatorach czy bazach danych. Paski postępu. Jeżeli program wykonuje sekwencję poleceń i każde z nich ma metodę oszacujCzasDoKońca(), można łatwo oszacować całkowity czas wszystkich operacji i pokazać dokładny pasek postępu.
Kreatory. Kreatory często pokazują wiele stron konfiguracji dla pojedynczej akcji, która jest realizowana dopiero, gdy użytkownik klika przycisk "Zakończ" na ostatniej stronie. Można zapisać akcję kreatora jako obiekt polecenia. Jest on tworzony, gdy okno kreatora jest po raz pierwszy wyświetlane. Każdy krok kreatora ustawia parametry w obiekcie polecenia. Przycisk "Zakończ" jedynie wywołuję metodę wykonaj(). W ten sposób klasa polecenia nie zawiera żadnego kodu interfejsu użytkownika.
Pule wątków. Typowa klasa puli wątków może mieć publiczną metodę dodajZadanie(), dodającą obiekty poleceń do wewnętrznej kolejki zadań do wykonania, a następnie ograniczona liczba wątków wykonuje po kolei te zadania. Te obiekty poleceń zwykle wypełniają wspólny interfejs jak na przykład java.lang.Runnable, który pozwala puli wątków wykonywać zadania bez jakiejkolwiek wiedzy o ich działaniu.
Nagrywanie makr. Jeżeli wszystkie działania użytkownika są reprezentowane przez obiekty poleceń, program może zapisać sekwencję akcji jako listę obiektów poleceń w miarę jak są wykonywane. Można później "odegrać" te same akcje, wykonując z powrotem po kolei te obiekty poleceń. Jeżeli program ma wbudowany silnik skryptów, każdy obiekt może implementować metodę doSkryptu() i akcje użytkowników mogą być w łatwy sposób zapisane jako skrypty. Sieć. Można przesyłać przez sieć całe obiekty poleceń i wykonywać je na innych komputerach, na przykład akcje wszystkich graczy w grach komputerowych.