Ask meWzór.6 Powiedzmy, że chcesz przypisać wartość do zmiennej. Wystarczy do tego prosty kod jak : a = 4

Zmienna została zmieniona, jednak nie ma żadnej historii, aby zapisać, że takie zdarzenie nastąpiło. Nikt nam przecież nie może dać poprzedniej wartości tej zmiennej. Nie możemy przecież zapisać i serializować faktu zmiany wartości.Czyż nie?

 

Co, jeśli chciałbyś cofnąć swoje działanie przypisania zmiennej. 

No cóż, skoro nie masz historii, nie masz informacji o poprzedniej wartości to zrobienie takiej operacji jest niemożliwe

Wzorzec projektowy "Command" stawia sobie ten właśnie cel. Zamiast działać na obiektach bezpośrednio, co by było, gdybyśmy mogli wysłać, im polecenia i instrukcje co trzeba zrobić.

Klasa reprezentująca tę polecenie będzie opisem tego, co trzeba zrobić i jak.

O co chodzi:

Stwórzmy model reprezentujący konto bankowe. W tym koncie bankowym możesz dodawać pieniądze bez limitu i możesz też wybierać pieniądze do granicy minus 100.

public class BankAccount
{
    private int balance;
    private int overdraftLimit = -100;

    public void Deposit(int amount)
    {
        balance += amount;
        Console.WriteLine
            ($"Deposited ${amount}, balance is now {balance}");
    }
    public void Withdraw(int amount)
    {
        if (balance - amount >= overdraftLimit)
        {
            balance -= amount;
            Console.WriteLine
                ($"Withdrew ${amount}, balance is now {balance}");
        }
    }

    public override string ToString()
    {
        return $"{nameof(balance)}: {balance}";
    }
}

Możemy wywołać te metody bezpośrednio, ale chcemy mieć informację o każdej operacji na tym koncie. 

Zaczynamy więc swoją przygodę ze wzorem Command. Tworzymy interfejs, który będzie implantował każde nasze polecenie.

public interface ICommand
{
    void Call();
}

Teraz tworzymy klasę , która będzie definiować nasze polecenie do banku. Polecenie te zawiera trzy informacje:

  • Co będziemy robić z tym kontem "WhatToDoWithBankAccount"
  • Konto, na którym będziemy operować : BankAccount
  • Ilość pieniędzy do danej operacji : Amount
public class BankAccountCommand : ICommand
{
    private BankAccount _account;
    
    public enum WhatToDoWithBankAccount
    {
        Deposit, Withdraw
    }
    
    private WhatToDoWithBankAccount _whattodo;
    private int _amount;
    
    public BankAccountCommand
    (BankAccount account, WhatToDoWithBankAccount
        whattodo, int amount)
    {
    
        _account = account;
        _whattodo = whattodo;
        _amount = amount;
    
    }

    public void Call()
    {
        throw new NotImplementedException();
    }
}

Pozostało nam uzupełnić kod wywołania polecenia.

Sprawdzamy jaką akcję mamy wykonać, a potem na samym koncie bankowym wykonujemy tę operację. 

public void Call()
{
    switch (_whattodo)
    {
        case WhatToDoWithBankAccount.Deposit:
            _account.Deposit(_amount);
            break;
        case WhatToDoWithBankAccount.Withdraw:
            _account.Withdraw(_amount);
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
}

Z odpowiednimi modyfikacjami nasz kod teraz działa tak:

var bank = new BankAccount();

var cmd = new BankAccountCommand(bank,
    WhatToDoWithBankAccount.Deposit, 2000);
cmd.Call();

var cmd2 = new BankAccountCommand(bank,
    WhatToDoWithBankAccount.Withdraw, 600);
cmd2.Call();

Console.WriteLine(bank);

Dodajemy do konta 2000, a potem zabieramy 600, co daje wartość 1400. 

Teraz, gdybym zapisał w jakiś sposób klasę obiektu BankAccountCommand , to miałbym ślad historii działań na moim koncie.

Często takie klasy poleceń są zapisywane w formacie XML lub JSON w bazie danych. Trzeba by było dodać do tej klasy oczywiście takie rzeczy jak :

  • Unikatowe ID
  • Datę wykonania
  • Przez kogo polecenie jest wykonane
  • i czy dane polecenie zostało wykonane z sukcesem.

Poza tym, jeśli Ciebie martwi fakt, że metody BankAccount wciąż są dostępne to jedyny sposób, aby tego uniknąć, to sprawić, aby BankAccountCommand dziedziczył po BankAccount, a same metody Withdraw,Deposit uczynić dostępne do poziomu protected.

Cofanie poleceń

Polecenie przechowuje nam więc wszystkie informacje na temat danej modyfikacji na koncie bankowym.

Skoro mamy te informację, oznacza to, że możemy cofnąć dane polecenie i przywrócić stan konta do poprzedniego.

Dodajemy więc nową metodę Undo.

public interface ICommand
{
    void Call();
    void Undo();
}

Chociaż, też zastanów się z tą zmianą. Pytanie, czy każde polecenie będzie miało możliwość cofania? Może wszystkie albo niektóre polecenia będą ostateczne.

Jeśli tak będzie to warto oddzielić ten interfejs do innego interfejsu jak : IUndoable.

A samo polecenie też wydzielić jako ICallable. 

A oto prosta metoda Undo do naszego konta bankowego. Wykonuje ona po prostu odwrotną operację do tej, która zaszła, aby przywrócić dawny stan. Nie zawsze będzie to możliwe i takie łatwe, ale hej jest to tylko przykład.

public void Undo()
{
    switch (_whattodo)
    {
        case WhatToDoWithBankAccount.Deposit:
            _account.Withdraw(_amount);
            break;
        case WhatToDoWithBankAccount.Withdraw:
            _account.Deposit(_amount);
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
}

Ta logika cofania także wymusza na nas ulepszenie pewnych naszych metod. Chcesz w końcu cofać polecenia, które wykonały się z sukcesem. 

Pamiętaj, że mamy logikę w kodzie, która mówi, że możemy mieć minimalnie -100 na swoim koncie. Jeśli będziemy chcieli zabrać więcej pieniędzy z konta, czyli odjąć pewną kwotę to ta operacja się nie uda.

Takie nieudane polecenie nie chcemy cofać.

Do metody wyciągania pieniędzy, dodajemy informację o obsłużeniu tej operacji.

public class BankAccount
{
    public bool Withdraw(int amount)
    {
        if (balance - amount >= overdraftLimit)
        {
            balance -= amount;
            Console.WriteLine($"Withdrew ${amount}, balance is now ${balance}");
            return true;
        }
        return false;
    }

Do samego polecenia dodajemy flagę o poprawnym wykonaniu danej metody.

public class BankAccountCommand : ICommand
{
    private bool _succeeded;

Zakładamy, że gdy wpłacamy pieniądze, to ta operacja zawsze zakończy się sukcesem. 

W metodzie Undo sprawdzamy najpierw, czy polecenie, które chcemy cofnąć,  w ogóle zakończyło się sukcesem.

public void Call()
{
    switch (_whattodo)
    {
        case WhatToDoWithBankAccount.Deposit:
            _account.Deposit(_amount);
            _succeeded = true;
            break;
        case WhatToDoWithBankAccount.Withdraw:
            _succeeded = _account.Withdraw(_amount);
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
}

public void Undo()
{
    if (!_succeeded) return;
    switch (_whattodo)
    {
        case WhatToDoWithBankAccount.Deposit:
            _succeeded = _account.Withdraw(_amount);
            break;
        case WhatToDoWithBankAccount.Withdraw:
            _account.Deposit(_amount);
            _succeeded = true;
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }

}

Teraz sprawdźmy, czy wszystko działa.

var bank = new BankAccount();

var cmd = new BankAccountCommand(bank,
    WhatToDoWithBankAccount.Deposit, 2000);
cmd.Call();

var cmd2 = new BankAccountCommand(bank,
    WhatToDoWithBankAccount.Withdraw, 600);
cmd2.Call();

Console.WriteLine(bank);

Z baku najpierw cofamy polecenie zebrania pieniędzy, więc kwota wraca do 1000. Potem cofamy wpłatę do konta i konto wraca do stanu początkowego, czyli 0.

Kompozyt poleceń : Composite Commands

Transfer pieniędzy z dwóch konto może być przetworzony przy pomocy dwóch poleceń:

  • Zabrania pieniędzy z konta numer 1
  • Oddania pieniędzy do konta numer 2

Jest to tak jakby jedna operacja, a więc dobrze by było zrobić to przy pomocy jednego polecenia,a nie dwóch

Wystarczy stworzyć jedno polecenie, które zawiera w sobie listę poleceń. W esencji jest to wzorzec projektowy Composite

Zdefiniujmy szkielet tej klasy. Dziedziczy on po liście poleceń, jak i definiuje on sam interfejs ICommand.

public abstract class CompositeBankAccountCommand :
    List<BankAccountCommand>, ICommand
{
    public virtual void Call()
    {
        ForEach(cmd => cmd.Call());
    }

    public virtual void Undo()
    {
        foreach (var cmd in 
            ((IEnumerable<BankAccountCommand>)this)
            .Reverse())
        {
            cmd.Undo();
        }
    }
}

Wzorzec projektowy Composite tworzy właśnie obiekt, który jest zarazem listą swoich definicji, a zarazem sam ją jest. Wiem, brzmi to dziwnie, ale patrząc na szkielet tej klasy abstrakcyjnej łatwiej jest to zrozumieć

Metoda Call wywoła wszystkie polecenia po kolej. Natomiast metoda Undo, która ma to wszystko cofnąć. Musi wykonać te polecenia cofania w odwrotnej kolejności.

Teraz stwórzmy klasę, która będzie transferować pieniądze pomiędzy dwoma kontami.

public class MoneyTransferCommand : CompositeBankAccountCommand
{
    public MoneyTransferCommand(BankAccount from,
    BankAccount to, int amount)
    {
        AddRange(new[]
        {
            new BankAccountCommand(from,
            BankAccountCommand.WhatToDoWithBankAccount.Withdraw, amount),
            new BankAccountCommand(to,
            BankAccountCommand.WhatToDoWithBankAccount.Deposit, amount)
        });
    }
}

Jak widzisz, nasza klasa działa : z konta 1 jest zabrany 1000

var from = new BankAccount();
from.Deposit(2000);
var to = new BankAccount();
var mtc = new MoneyTransferCommand(from, to, 1000);
mtc.Call();
Console.WriteLine(from);
Console.WriteLine(to);

Istnieje oczywiście pewien problem. Co, jeśli nie będziesz mógł zabrać pieniędzy z pierwszego konta? Oznacza to, że nie powinieneś wykonać operacji dodania pieniędzy. 

Co teraz? Będzie nam potrzebna jawna informacja czy dane polecenie zostało wykonane poprawnie. Dodaje więc właściwość do interfejsu Command.

public interface ICommand
{
    void Call();
    void Undo();


    bool Succeeded { get; }
}

Teraz będę mógł sprawdzić stan każdego polecenia, ponieważ ta właściwość jest publiczna

public class BankAccountCommand : ICommand
{
    private bool _succeeded = false;
    
    public bool Succeeded { get { return _succeeded; } }

Do naszego kompozytu muszę napisać obsługę cofania zbioru zdarzeń, jeśli chociaż jedno z poprzednich zadań wykonało się nieprawidłowo. 

Jest to jak sesja w bazach danych gdzie, gdy coś pójdzie nie tak, to cofamy wszystkie polecenie, które zrobiliśmy. 

public abstract class CompositeBankAccountCommand :
    List<BankAccountCommand>, ICommand
{
    private bool _succeeded = false;

    public bool Succeeded { get { return _succeeded; } }

    public virtual void Call()
    {
        List<BankAccountCommand> previous = new List<BankAccountCommand>();
        BankAccountCommand prev = null;

        foreach (var item in
            (IEnumerable<BankAccountCommand>)this)
        {
            if (prev != null)
            {
                if (prev.Succeeded)
                {
                    item.Call();
                }
                else
                {
                    foreach (var previousItem in previous)
                    {
                        previousItem.Undo();
                    }
                    _succeeded = false;
                    break;
                }
            }
            else
            {
                item.Call();
            }

            prev = item;
            previous.Add(item);
        }
    }

    public virtual void Undo()
    {
        foreach (var cmd in
            ((IEnumerable<BankAccountCommand>)this)
            .Reverse())
        {
            cmd.Undo();
        }
    }
}

Aby było to proste, to teraz nie bawię się już w żadne wzorce projektowe. Mam po prostu dwa obiekty, które przechowują mi poprzednie polecenie, jak i listę wszystkich poprzednich poleceń, jakie zrobiłem do tej pory.

var from = new BankAccount();
from.Deposit(2000);
var to = new BankAccount();
var mtc = new MoneyTransferCommand(from, to, 2500);
mtc.Call();
Console.WriteLine(from);
Console.WriteLine(to);

Gdy spróbujesz pobrać z konta, więcej pieniędzy niż jest, to te polecenie zbiorowe też się nie wykona.

CQS : Queries and Command Query Separation

Wraz z tym wzorcem projektowym powstała idea CQS. Jest to idea dzielenia operacji w systemie na dwie kategorie :

Commands: są to instrukcje do systemu, które wykonują operację i modyfikują stan, ale nie zwracają żadnej wartości. Queries: są to zapytania o informację danego systemu. Nie zmieniają one stanu systemu w żaden sposób.

Biblia wzorców projektowych (https://en.wikipedia.org/wiki/Design_Patterns) nie definiuje Query jako oddzielnego wzorca. Zapytania Query po prostu zwracają wartości i wciąż działa na szablonie wzorca Command.

Podsumowanie

Klasy więc mogą gadać do siebie, wysyłając odpowiednie obiekty instrukcji. 

Czasami chcesz by ten obiekt, był tylko instrukcją. By zrobił zapytanie i coś zwrócił. By zmienił stan systemu. W innych wzorcach projektowych jak Event Broker chcesz by ten obiekt polecenia, także modyfikował rezultat, który ma być zwracany.

Sam obiekt trzymająca ten rezultat się nie zmienia, ale rezultat przekazany już tak.Jak można się domyślić, polecenia są używane w środowiskach UI. W końcu możesz cofać CTRL+Z swoje zmiany nawet w notatniku

Same polecenia mogą być łączone ze sobą, tworząc kompozyty poleceń. Co więcej, kompozyt poleceń może trzymać kolejny kompozyt.

Z wszystkich wzorców projektowych ten wydaje się najbardziej obecny i użyteczny w programowaniu.