Autor: Arkadiusz Kotarski


Wprowadzenie

Jednym z prostszych wzorców projektowych jest Łańcuch Zobowiązań (zwanym również Łańcuchem Odpowiedzialności). Jego przeznaczenie jest mało skomplikowane - w związku z tym jest on prosty do opanowania nawet przez początkujących programistów. Nie ukrywam - był to także mój wybór .

Zalicza się do wzorców operacyjnych, ułatwia dzielenie odpowiedzialności między klasami w skutek czego pomaga zastosować sie do pierwszej zasady SOLID czyli pojedyńczej odpowiedzialności. Drugą zaletą jest to że z łatwością można dodać kolejne klasy w hierarchi, które mogą przechwycić żądanie bez zmiany dotychczasowego kodu. Głównym problemem płynącym z nie używania Łańcucha zobowiązań może być niewłaściwe zarządzanie zależnościami między klasami.

Z uwagi na to, że dany algorytm pracuje na hierarchi bardzo dobrze sprawdza sie w połączeniu z Kompozytem. Więcej o nim możecie przeczytać w moim poprzednim wpisie: Wzorce projektowe - Kompozyt

Wzorzec projektowy - Łańcuch Odpowiedzialności

Problem

Po raz kolejny mój przykład będzie bazował na grze strategicznej. Kiedy zaznaczymy podstawową jednostkę jaką jest robotnik, ukazuje się dodatkowe menu przypisanych do niego akcji, tj. możemy wybrać dowolne polecenie z listy i wydać rozkaz do jego wykonania.

Drugą ważną mechaniką w grach tego gatunku jest atakowanie przeciwników. Po zaznaczeniu wojownika mamy możliwość zaatakowania wrogich jednostek. Kiedy jednak poruszamy mieszanym oddziałem zazwyczaj wygląda to tak, że nie możemy wykonać żadnych spersonalizowanych poleceń tylko rozkaz do przemieszczenia się.

Dzięki omawianemu dzisiaj wzorcowi projektowemu zyskujemy sposobność wyświetlenia wszystkich umiejętności zaznaczonych postaci. Algorytm przekaże żadanie do pierwszej jednostki a ta jeżeli nie będzie w stanie tego wykonać przydzieli do następnej, aż zadanie zostanie wykonane.

Poniżej diagram dla mojej implementacji: Wzorzec projektowy - Łańcuch Odpowiedzialności

Implementacja

// Przykładowe zadania jakie jednostki mogą wykonać
public enum Request
{
    House,
    Portal,
    Attack
}

// Główna klasa aplikacji konsolowej
class Program
{
    static void Main(string[] args)
    {   
           
        List<Unit> selectedUnits = new List<Unit>();

        // Przez to, że nie wiemy jakie obiekty znajdują się w zaznaczeniu
        // dokonujemy symulacji takiej sytuacji
        // poniżej dodaje obiekty do listy
        selectedUnits.Add(new Worker());
        selectedUnits.Add(new Warrior());
        selectedUnits.Add(new Mag());

        // Przeszukuje listę wszystkich zaznaczonych jednostek 
        // a następnie dokonuję powiazuje ich ze soba w lańcuch
        for (int i = 0; i < selectedUnits.Count - 1; i++)
        {
            selectedUnits[i].setNumber(selectedUnits[i + 1]);
        }

        // Zadania do wykonania
        List<Request> Requests = new List<Request> {
            Request.House,
            Request.Portal,
            Request.Attack
        };

        // Wysłanie żądań
        foreach (var request in Requests)
        {
            selectedUnits[0].HandleRequest(request);
        }

        // Zatrzymanie aplikacji
        Console.ReadKey();
    }
}

// Abstrakcyjna klasa odpowiedzialna za połączenie klas w łańcuch
abstract class Unit
{
    protected Unit unit;

    public void setNumber(Unit unit)
    {
        this.unit = unit;
    }

    public abstract void HandleRequest(Request request);
}

// Konkretny handler czyli klasa, która przetwarza żądanie lub wysyła dalej
class Worker : Unit
{
    // sprawdzanie czy klasa jest w stanie przetworzyć żądanie czy wysłać je dalej
    public override void HandleRequest(Request request)
    {
        if (request == Request.House)
        {
            Console.WriteLine("The worker start making a house");
        }
        else if (unit != null)
        {
            unit.HandleRequest(request);
        }
    }
}

// Konkretny handler czyli klasa, która przetwarza żądanie lub wysyła dalej
class Warrior : Unit
{
    //sprawdzanie czy klasa jest w stanie przetworzyć żądanie czy wysłać je dalej
    public override void HandleRequest(Request request)
    {
        if (request == Request.Attack)
        {
            Console.WriteLine("The warrior start attacking");
        }
        else if (unit != null)
        {
            unit.HandleRequest(request);
        }
    }
}

// Konkretny handler czyli klasa, która przetwarza żądanie lub wysyła dalej
class Mag : Unit
{
    //sprawdzanie czy klasa jest w stanie przetworzyć żądanie czy wysłać je dalej
    public override void HandleRequest(Request request)
    {
        if (request == Request.Portal)
        {
            Console.WriteLine("The mag start casting a portal");
        }
        else if (unit != null)
        {
            unit.HandleRequest(request);
        }
    }
}

                

Podsumowanie

Łańcuch zobowiązań jest przyjemnym i łatwym w opanowaniu wzorcem. Same klasy nie są rozbudowane - tak na dobrą sprawę posiadają one tylko jedną główną metodę.

Potrafi ona oddzielić zależności między klasami – nie muszą wiedzieć o sobie nawazajem. Oczywiście główną wadą tego wzorca jest to, że żądanie może nie zostać obsłużone, a co za tym idzie musimy o tym pamiętać i zabezpieczyć kod przed taką ewentualnością.